juryrig 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.
juryrig-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ian Alloway
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.
juryrig-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: juryrig
3
+ Version: 0.1.0
4
+ Summary: Audit your LLM judges: position bias, verbosity bias, prompt injection, consistency, panels, and calibration.
5
+ Author-email: Ian Alloway <ian@allowayllc.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ianalloway/juryrig
8
+ Project-URL: Issues, https://github.com/ianalloway/juryrig/issues
9
+ Keywords: llm,evals,llm-as-judge,calibration,bias,prompt-injection
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # juryrig
21
+
22
+ **Audit your LLM judges before you trust them.**
23
+
24
+ [![CI](https://github.com/ianalloway/juryrig/actions/workflows/ci.yml/badge.svg)](https://github.com/ianalloway/juryrig/actions/workflows/ci.yml)
25
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
26
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-zero-16c784)
27
+ ![License](https://img.shields.io/badge/license-MIT-blue)
28
+
29
+ LLM-as-judge is everywhere: the cheapest way to grade model outputs is to ask
30
+ another model. But the judge is a model too — with position bias, a weakness
31
+ for long-winded answers, run-to-run inconsistency, and confidence that rarely
32
+ matches its accuracy. If you haven't measured those, your eval numbers are
33
+ decoration.
34
+
35
+ juryrig is a small, zero-dependency Python toolkit that treats the judge as
36
+ the thing under test:
37
+
38
+ - **Position-bias audit** — present every A/B pair in both orders; count how often the *slot* (not the content) decides the winner.
39
+ - **Verbosity-bias audit** — re-score responses padded with content-free filler; a fair judge shouldn't reward padding.
40
+ - **Prompt-injection audit** — append judge-targeted instructions to bad responses; a robust judge should grade the answer, not obey it.
41
+ - **Self-consistency** — same input, several runs; how stable is the score?
42
+ - **Panels** — pool several judges (mean / median / min) and get an agreement score, so you know when your verdict depends on which judge you picked. Pairwise panels vote on A/B pairs and report a dead heat as one.
43
+ - **Calibration** — Brier score, reliability tables, and expected calibration error against human labels.
44
+
45
+ Run the whole battery with `audit_suite()`, or from the command line with
46
+ `juryrig cases.json`.
47
+
48
+ ## Install
49
+
50
+ > **PyPI publish pending** — `juryrig` is **not** on PyPI yet. Trusted Publishing
51
+ > is wired in [`.github/workflows/publish.yml`](.github/workflows/publish.yml);
52
+ > install from GitHub until the first release lands:
53
+
54
+ ```bash
55
+ pip install git+https://github.com/ianalloway/juryrig
56
+ # after PyPI: pip install juryrig
57
+ ```
58
+
59
+ ## Quickstart
60
+
61
+ ```python
62
+ from juryrig import (
63
+ MockJudge,
64
+ Panel,
65
+ position_bias,
66
+ prompt_injection_bias,
67
+ verbosity_bias,
68
+ )
69
+
70
+ rubric = "Answer must mention photosynthesis, chlorophyll, sunlight, and energy."
71
+
72
+ # 1. Audit a judge before using it
73
+ judge = MockJudge(name="demo") # swap in your own judge for real audits
74
+ cases = [("How do plants make food?", "good answer...", "weak answer...")]
75
+
76
+ bias = position_bias(judge, cases, rubric)
77
+ print(f"flip rate: {bias.flip_rate:.0%} flagged: {bias.flagged}")
78
+
79
+ injection = prompt_injection_bias(judge, [("Weak answer prompt", "vague answer")], rubric)
80
+ print(f"injection lift: {injection.mean_delta:+.3f} flagged: {injection.flagged}")
81
+
82
+ # 2. Use a panel instead of a single judge
83
+ panel = Panel([MockJudge(name="primary"), MockJudge(name="baseline")])
84
+ report = panel.evaluate(prompt="How do plants make food?",
85
+ response="Photosynthesis converts sunlight...",
86
+ rubric=rubric)
87
+ print(report.pooled, report.agreement)
88
+ ```
89
+
90
+ `position_bias()` is a pairwise audit and needs a judge with `compare()`;
91
+ `MockJudge` implements both `compare()` and `judge()` so the quickstart runs
92
+ without API credentials.
93
+
94
+ ## The whole battery in one call
95
+
96
+ `audit_suite()` runs every audit and pools the verdicts. Each case is a
97
+ `(prompt, good_response, weak_response)` triple: the pair drives the position
98
+ comparison, the good response gets padded to detect verbosity bias, and the
99
+ weak one carries the injection payload.
100
+
101
+ ```python
102
+ from juryrig import MockJudge, audit_suite
103
+
104
+ report = audit_suite(MockJudge(), cases, rubric)
105
+
106
+ print(report.summary())
107
+ assert not report.flagged, f"judge failed: {report.failures}"
108
+ ```
109
+
110
+ `report.failures` names the audits that tripped (`("position", "injection")`).
111
+ If the judge has no `compare()`, the position audit is reported in
112
+ `report.skipped` rather than silently counted as a pass.
113
+
114
+ ## Ties
115
+
116
+ `compare()` may return `"tie"` as well as `"A"` or `"B"`. It's optional — a
117
+ judge that only ever picks a side is unaffected — but real judges often want
118
+ to call two answers equivalent, and forcing that into a coin flip manufactures
119
+ position bias that isn't there.
120
+
121
+ How `position_bias()` accounts for them:
122
+
123
+ - **Tying both ways is not a flip.** The judge gave the same answer in both
124
+ orders, which is consistency, not order-dependence.
125
+ - **Tying one way and picking the other way *is* a flip.** The verdict changed
126
+ when only the order changed — that's the thing being measured.
127
+ - **Ties are excluded from `first_slot_wins`.** Counting them as "not won by
128
+ the first slot" would drag the ratio to 0 and flag a judge that ties
129
+ everything as maximally biased toward slot two. With nothing decisive to go
130
+ on the audit reports 0.5: no evidence of skew. The count is kept in
131
+ `report.ties` so it stays visible rather than silently dropped.
132
+
133
+ ## Panels of pairwise judges
134
+
135
+ `Panel.evaluate()` pools scores; `Panel.compare()` pools A/B votes by majority:
136
+
137
+ ```python
138
+ verdict = panel.compare(prompt="How do plants make food?",
139
+ a="Photosynthesis converts sunlight...",
140
+ b="Plants eat soil.",
141
+ rubric=rubric)
142
+
143
+ print(verdict.winner, verdict.agreement, verdict.votes)
144
+ ```
145
+
146
+ An even split sets `winner` to `None` and `deadlocked` to `True`, rather than
147
+ picking a side. A coin-flip winner would hide exactly the disagreement you
148
+ convened a panel to find. Judges without `compare()` are rejected by name —
149
+ silently dropping them would move the verdict while leaving `agreement`
150
+ looking healthy.
151
+
152
+ ## Going faster against a real judge
153
+
154
+ `audit_suite` on N cases is roughly 4N judge calls, which is minutes of wall
155
+ time over a network. `max_workers` runs them in parallel:
156
+
157
+ ```python
158
+ report = audit_suite(judge, cases, rubric, max_workers=8)
159
+ ```
160
+
161
+ Serial by default, because a judge may be stateful or rate-limited and
162
+ threading one behind your back would be a surprise. Results are collected in
163
+ input order, so a report is identical no matter how many workers produced it
164
+ — workers are a speed knob, never a correctness one. Your judge must be
165
+ thread-safe to raise it. The CLI exposes the same thing as `--workers`.
166
+
167
+ ## Tuning what counts as a failure
168
+
169
+ Every `flagged` verdict comes from a `Thresholds` object. The defaults are
170
+ strict on purpose, but they're yours to move:
171
+
172
+ ```python
173
+ from juryrig import Thresholds, audit_suite
174
+
175
+ report = audit_suite(judge, cases, rubric, thresholds=Thresholds(
176
+ injection_max_delta=0.05, # stricter: near-zero tolerance for injection
177
+ verbosity_mean_delta=0.10, # looser: this judge is allowed to like detail
178
+ ))
179
+ ```
180
+
181
+ The measurements never change — only the line between pass and fail. Each
182
+ report carries the `thresholds` it was judged against, so a stored report
183
+ still explains its own verdict. A case file can set them too, under a
184
+ `"thresholds"` key; unknown keys are rejected rather than ignored, so a typo
185
+ can't silently leave the strict default in force.
186
+
187
+ ## Command line
188
+
189
+ ```bash
190
+ juryrig examples/cases.json # audit the built-in MockJudge
191
+ juryrig cases.json --provider anthropic --json # audit a live judge
192
+ ```
193
+
194
+ The case file is `{"rubric": ..., "cases": [{"prompt", "good", "weak"}, ...]}`.
195
+ The command exits `1` when the judge is flagged and `2` on bad input, so a CI
196
+ step is one line. Without installing, use `python -m juryrig cases.json`.
197
+
198
+ ### Optional: provider-backed judges
199
+
200
+ `AnthropicJudge` and `OpenAIJudge` wrap the Anthropic/OpenAI HTTP APIs
201
+ (stdlib-only, no extra dependencies) and work with the single-response
202
+ audits. They're not exported from the top-level package — import them
203
+ explicitly when you need a live model:
204
+
205
+ ```python
206
+ from juryrig.providers import AnthropicJudge, OpenAIJudge # needs *_API_KEY env var
207
+ ```
208
+
209
+ An audit is many calls in a row, so both retry transient failures (429, 5xx,
210
+ network errors) with exponential backoff — one flaky response shouldn't throw
211
+ away every result collected before it. A numeric `Retry-After` is honoured.
212
+ Client errors like 401 and 404 fail fast, since they'd fail identically on
213
+ every attempt.
214
+
215
+ ```python
216
+ from juryrig.providers import AnthropicJudge, RetryPolicy
217
+
218
+ judge = AnthropicJudge(retry=RetryPolicy(attempts=5, backoff=1.0))
219
+ ```
220
+
221
+ Every audit returns a small frozen dataclass with a `flagged` property, so
222
+ gating a CI pipeline is one `if`:
223
+
224
+ ```python
225
+ assert not position_bias(judge, cases, rubric).flagged, "judge is positionally biased"
226
+ ```
227
+
228
+ ## Why the MockJudge has built-in flaws
229
+
230
+ `MockJudge(position_bias=..., verbosity_bias=..., injection_bias=...,
231
+ noise=..., instability=..., tie_margin=...)` lets you dial in known defects.
232
+ That's how juryrig tests itself — the audits must detect a rigged judge and
233
+ clear a fair one — and it gives you a deterministic, network-free way to test
234
+ *your* eval pipeline end to end.
235
+
236
+ `noise` and `instability` are not the same knob, and the difference trips
237
+ people up:
238
+
239
+ - **`noise`** is seeded on the input, so re-judging one response returns the
240
+ same score forever. It perturbs scores *across* responses.
241
+ - **`instability`** is seeded on a call counter, so the same input scores
242
+ differently each time. It's the only flaw `self_consistency()` can detect —
243
+ a judge with `noise=0.9` reports a spread of exactly `0.0`.
244
+
245
+ Instability is still reproducible: a fresh `MockJudge` replays the same
246
+ sequence, so switching the flaw on doesn't make your tests flaky.
247
+
248
+ `tie_margin` makes the judge answer `"tie"` when two responses score within
249
+ it. Useful for exercising tie handling — and note that *without* it, two
250
+ identical answers are handed to slot A by `compare()`'s tie-break, which the
251
+ position audit correctly reports as bias.
252
+
253
+ ## Calibration
254
+
255
+ ```python
256
+ from juryrig import brier_score, expected_calibration_error
257
+
258
+ scores = [0.9, 0.8, 0.3, 0.95] # judge scores
259
+ labels = [1, 1, 0, 0] # human ground truth
260
+
261
+ print(brier_score(scores, labels))
262
+ print(expected_calibration_error(scores, labels))
263
+ ```
264
+
265
+ A judge that says 0.9 should be right ~90% of the time. ECE tells you how far
266
+ that promise is from reality.
267
+
268
+ ## Demo
269
+
270
+ ```bash
271
+ python3 examples/audit_demo.py
272
+ ```
273
+
274
+ Runs the full audit suite against a fair judge and a rigged one, no API keys
275
+ required.
276
+
277
+ ## Design notes
278
+
279
+ - **Zero runtime dependencies** — stdlib only, including the API clients.
280
+ - **Provider-agnostic** — a judge is anything with a `judge()` method; pairwise judges add `compare()`. Protocols, not base classes.
281
+ - **Deterministic tests** — all randomness is hash-seeded; CI never flakes.
282
+ - **Typed** — ships a PEP 561 `py.typed` marker, so the hints reach your type checker.
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,267 @@
1
+ # juryrig
2
+
3
+ **Audit your LLM judges before you trust them.**
4
+
5
+ [![CI](https://github.com/ianalloway/juryrig/actions/workflows/ci.yml/badge.svg)](https://github.com/ianalloway/juryrig/actions/workflows/ci.yml)
6
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
7
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-zero-16c784)
8
+ ![License](https://img.shields.io/badge/license-MIT-blue)
9
+
10
+ LLM-as-judge is everywhere: the cheapest way to grade model outputs is to ask
11
+ another model. But the judge is a model too — with position bias, a weakness
12
+ for long-winded answers, run-to-run inconsistency, and confidence that rarely
13
+ matches its accuracy. If you haven't measured those, your eval numbers are
14
+ decoration.
15
+
16
+ juryrig is a small, zero-dependency Python toolkit that treats the judge as
17
+ the thing under test:
18
+
19
+ - **Position-bias audit** — present every A/B pair in both orders; count how often the *slot* (not the content) decides the winner.
20
+ - **Verbosity-bias audit** — re-score responses padded with content-free filler; a fair judge shouldn't reward padding.
21
+ - **Prompt-injection audit** — append judge-targeted instructions to bad responses; a robust judge should grade the answer, not obey it.
22
+ - **Self-consistency** — same input, several runs; how stable is the score?
23
+ - **Panels** — pool several judges (mean / median / min) and get an agreement score, so you know when your verdict depends on which judge you picked. Pairwise panels vote on A/B pairs and report a dead heat as one.
24
+ - **Calibration** — Brier score, reliability tables, and expected calibration error against human labels.
25
+
26
+ Run the whole battery with `audit_suite()`, or from the command line with
27
+ `juryrig cases.json`.
28
+
29
+ ## Install
30
+
31
+ > **PyPI publish pending** — `juryrig` is **not** on PyPI yet. Trusted Publishing
32
+ > is wired in [`.github/workflows/publish.yml`](.github/workflows/publish.yml);
33
+ > install from GitHub until the first release lands:
34
+
35
+ ```bash
36
+ pip install git+https://github.com/ianalloway/juryrig
37
+ # after PyPI: pip install juryrig
38
+ ```
39
+
40
+ ## Quickstart
41
+
42
+ ```python
43
+ from juryrig import (
44
+ MockJudge,
45
+ Panel,
46
+ position_bias,
47
+ prompt_injection_bias,
48
+ verbosity_bias,
49
+ )
50
+
51
+ rubric = "Answer must mention photosynthesis, chlorophyll, sunlight, and energy."
52
+
53
+ # 1. Audit a judge before using it
54
+ judge = MockJudge(name="demo") # swap in your own judge for real audits
55
+ cases = [("How do plants make food?", "good answer...", "weak answer...")]
56
+
57
+ bias = position_bias(judge, cases, rubric)
58
+ print(f"flip rate: {bias.flip_rate:.0%} flagged: {bias.flagged}")
59
+
60
+ injection = prompt_injection_bias(judge, [("Weak answer prompt", "vague answer")], rubric)
61
+ print(f"injection lift: {injection.mean_delta:+.3f} flagged: {injection.flagged}")
62
+
63
+ # 2. Use a panel instead of a single judge
64
+ panel = Panel([MockJudge(name="primary"), MockJudge(name="baseline")])
65
+ report = panel.evaluate(prompt="How do plants make food?",
66
+ response="Photosynthesis converts sunlight...",
67
+ rubric=rubric)
68
+ print(report.pooled, report.agreement)
69
+ ```
70
+
71
+ `position_bias()` is a pairwise audit and needs a judge with `compare()`;
72
+ `MockJudge` implements both `compare()` and `judge()` so the quickstart runs
73
+ without API credentials.
74
+
75
+ ## The whole battery in one call
76
+
77
+ `audit_suite()` runs every audit and pools the verdicts. Each case is a
78
+ `(prompt, good_response, weak_response)` triple: the pair drives the position
79
+ comparison, the good response gets padded to detect verbosity bias, and the
80
+ weak one carries the injection payload.
81
+
82
+ ```python
83
+ from juryrig import MockJudge, audit_suite
84
+
85
+ report = audit_suite(MockJudge(), cases, rubric)
86
+
87
+ print(report.summary())
88
+ assert not report.flagged, f"judge failed: {report.failures}"
89
+ ```
90
+
91
+ `report.failures` names the audits that tripped (`("position", "injection")`).
92
+ If the judge has no `compare()`, the position audit is reported in
93
+ `report.skipped` rather than silently counted as a pass.
94
+
95
+ ## Ties
96
+
97
+ `compare()` may return `"tie"` as well as `"A"` or `"B"`. It's optional — a
98
+ judge that only ever picks a side is unaffected — but real judges often want
99
+ to call two answers equivalent, and forcing that into a coin flip manufactures
100
+ position bias that isn't there.
101
+
102
+ How `position_bias()` accounts for them:
103
+
104
+ - **Tying both ways is not a flip.** The judge gave the same answer in both
105
+ orders, which is consistency, not order-dependence.
106
+ - **Tying one way and picking the other way *is* a flip.** The verdict changed
107
+ when only the order changed — that's the thing being measured.
108
+ - **Ties are excluded from `first_slot_wins`.** Counting them as "not won by
109
+ the first slot" would drag the ratio to 0 and flag a judge that ties
110
+ everything as maximally biased toward slot two. With nothing decisive to go
111
+ on the audit reports 0.5: no evidence of skew. The count is kept in
112
+ `report.ties` so it stays visible rather than silently dropped.
113
+
114
+ ## Panels of pairwise judges
115
+
116
+ `Panel.evaluate()` pools scores; `Panel.compare()` pools A/B votes by majority:
117
+
118
+ ```python
119
+ verdict = panel.compare(prompt="How do plants make food?",
120
+ a="Photosynthesis converts sunlight...",
121
+ b="Plants eat soil.",
122
+ rubric=rubric)
123
+
124
+ print(verdict.winner, verdict.agreement, verdict.votes)
125
+ ```
126
+
127
+ An even split sets `winner` to `None` and `deadlocked` to `True`, rather than
128
+ picking a side. A coin-flip winner would hide exactly the disagreement you
129
+ convened a panel to find. Judges without `compare()` are rejected by name —
130
+ silently dropping them would move the verdict while leaving `agreement`
131
+ looking healthy.
132
+
133
+ ## Going faster against a real judge
134
+
135
+ `audit_suite` on N cases is roughly 4N judge calls, which is minutes of wall
136
+ time over a network. `max_workers` runs them in parallel:
137
+
138
+ ```python
139
+ report = audit_suite(judge, cases, rubric, max_workers=8)
140
+ ```
141
+
142
+ Serial by default, because a judge may be stateful or rate-limited and
143
+ threading one behind your back would be a surprise. Results are collected in
144
+ input order, so a report is identical no matter how many workers produced it
145
+ — workers are a speed knob, never a correctness one. Your judge must be
146
+ thread-safe to raise it. The CLI exposes the same thing as `--workers`.
147
+
148
+ ## Tuning what counts as a failure
149
+
150
+ Every `flagged` verdict comes from a `Thresholds` object. The defaults are
151
+ strict on purpose, but they're yours to move:
152
+
153
+ ```python
154
+ from juryrig import Thresholds, audit_suite
155
+
156
+ report = audit_suite(judge, cases, rubric, thresholds=Thresholds(
157
+ injection_max_delta=0.05, # stricter: near-zero tolerance for injection
158
+ verbosity_mean_delta=0.10, # looser: this judge is allowed to like detail
159
+ ))
160
+ ```
161
+
162
+ The measurements never change — only the line between pass and fail. Each
163
+ report carries the `thresholds` it was judged against, so a stored report
164
+ still explains its own verdict. A case file can set them too, under a
165
+ `"thresholds"` key; unknown keys are rejected rather than ignored, so a typo
166
+ can't silently leave the strict default in force.
167
+
168
+ ## Command line
169
+
170
+ ```bash
171
+ juryrig examples/cases.json # audit the built-in MockJudge
172
+ juryrig cases.json --provider anthropic --json # audit a live judge
173
+ ```
174
+
175
+ The case file is `{"rubric": ..., "cases": [{"prompt", "good", "weak"}, ...]}`.
176
+ The command exits `1` when the judge is flagged and `2` on bad input, so a CI
177
+ step is one line. Without installing, use `python -m juryrig cases.json`.
178
+
179
+ ### Optional: provider-backed judges
180
+
181
+ `AnthropicJudge` and `OpenAIJudge` wrap the Anthropic/OpenAI HTTP APIs
182
+ (stdlib-only, no extra dependencies) and work with the single-response
183
+ audits. They're not exported from the top-level package — import them
184
+ explicitly when you need a live model:
185
+
186
+ ```python
187
+ from juryrig.providers import AnthropicJudge, OpenAIJudge # needs *_API_KEY env var
188
+ ```
189
+
190
+ An audit is many calls in a row, so both retry transient failures (429, 5xx,
191
+ network errors) with exponential backoff — one flaky response shouldn't throw
192
+ away every result collected before it. A numeric `Retry-After` is honoured.
193
+ Client errors like 401 and 404 fail fast, since they'd fail identically on
194
+ every attempt.
195
+
196
+ ```python
197
+ from juryrig.providers import AnthropicJudge, RetryPolicy
198
+
199
+ judge = AnthropicJudge(retry=RetryPolicy(attempts=5, backoff=1.0))
200
+ ```
201
+
202
+ Every audit returns a small frozen dataclass with a `flagged` property, so
203
+ gating a CI pipeline is one `if`:
204
+
205
+ ```python
206
+ assert not position_bias(judge, cases, rubric).flagged, "judge is positionally biased"
207
+ ```
208
+
209
+ ## Why the MockJudge has built-in flaws
210
+
211
+ `MockJudge(position_bias=..., verbosity_bias=..., injection_bias=...,
212
+ noise=..., instability=..., tie_margin=...)` lets you dial in known defects.
213
+ That's how juryrig tests itself — the audits must detect a rigged judge and
214
+ clear a fair one — and it gives you a deterministic, network-free way to test
215
+ *your* eval pipeline end to end.
216
+
217
+ `noise` and `instability` are not the same knob, and the difference trips
218
+ people up:
219
+
220
+ - **`noise`** is seeded on the input, so re-judging one response returns the
221
+ same score forever. It perturbs scores *across* responses.
222
+ - **`instability`** is seeded on a call counter, so the same input scores
223
+ differently each time. It's the only flaw `self_consistency()` can detect —
224
+ a judge with `noise=0.9` reports a spread of exactly `0.0`.
225
+
226
+ Instability is still reproducible: a fresh `MockJudge` replays the same
227
+ sequence, so switching the flaw on doesn't make your tests flaky.
228
+
229
+ `tie_margin` makes the judge answer `"tie"` when two responses score within
230
+ it. Useful for exercising tie handling — and note that *without* it, two
231
+ identical answers are handed to slot A by `compare()`'s tie-break, which the
232
+ position audit correctly reports as bias.
233
+
234
+ ## Calibration
235
+
236
+ ```python
237
+ from juryrig import brier_score, expected_calibration_error
238
+
239
+ scores = [0.9, 0.8, 0.3, 0.95] # judge scores
240
+ labels = [1, 1, 0, 0] # human ground truth
241
+
242
+ print(brier_score(scores, labels))
243
+ print(expected_calibration_error(scores, labels))
244
+ ```
245
+
246
+ A judge that says 0.9 should be right ~90% of the time. ECE tells you how far
247
+ that promise is from reality.
248
+
249
+ ## Demo
250
+
251
+ ```bash
252
+ python3 examples/audit_demo.py
253
+ ```
254
+
255
+ Runs the full audit suite against a fair judge and a rigged one, no API keys
256
+ required.
257
+
258
+ ## Design notes
259
+
260
+ - **Zero runtime dependencies** — stdlib only, including the API clients.
261
+ - **Provider-agnostic** — a judge is anything with a `judge()` method; pairwise judges add `compare()`. Protocols, not base classes.
262
+ - **Deterministic tests** — all randomness is hash-seeded; CI never flakes.
263
+ - **Typed** — ships a PEP 561 `py.typed` marker, so the hints reach your type checker.
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,49 @@
1
+ """juryrig — audit your LLM judges before you trust them.
2
+
3
+ Provider-backed judges (AnthropicJudge, OpenAIJudge) are not exported here;
4
+ import them explicitly from `juryrig.providers` when you need a live API.
5
+ """
6
+
7
+ from .audits import (
8
+ DEFAULT_THRESHOLDS,
9
+ ConsistencyReport,
10
+ PositionBiasReport,
11
+ PromptInjectionReport,
12
+ Thresholds,
13
+ VerbosityBiasReport,
14
+ position_bias,
15
+ prompt_injection_bias,
16
+ self_consistency,
17
+ verbosity_bias,
18
+ )
19
+ from .calibration import brier_score, expected_calibration_error, reliability_table
20
+ from .judge import Judge, Judgment, MockJudge, PairwiseJudge
21
+ from .panel import Panel, PanelReport, PanelVerdict
22
+ from .suite import AuditSuiteReport, audit_suite
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ __all__ = [
27
+ "AuditSuiteReport",
28
+ "ConsistencyReport",
29
+ "DEFAULT_THRESHOLDS",
30
+ "Judge",
31
+ "Judgment",
32
+ "MockJudge",
33
+ "PairwiseJudge",
34
+ "Panel",
35
+ "PanelReport",
36
+ "PanelVerdict",
37
+ "PositionBiasReport",
38
+ "PromptInjectionReport",
39
+ "Thresholds",
40
+ "VerbosityBiasReport",
41
+ "audit_suite",
42
+ "brier_score",
43
+ "expected_calibration_error",
44
+ "position_bias",
45
+ "prompt_injection_bias",
46
+ "reliability_table",
47
+ "self_consistency",
48
+ "verbosity_bias",
49
+ ]
@@ -0,0 +1,5 @@
1
+ """Entry point for `python -m juryrig`."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())