probatio-llm 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.
- probatio_llm-0.1.0/.gitignore +35 -0
- probatio_llm-0.1.0/CHANGELOG.md +147 -0
- probatio_llm-0.1.0/LICENSE +21 -0
- probatio_llm-0.1.0/PKG-INFO +326 -0
- probatio_llm-0.1.0/README.md +290 -0
- probatio_llm-0.1.0/pyproject.toml +98 -0
- probatio_llm-0.1.0/src/probatio/__init__.py +69 -0
- probatio_llm-0.1.0/src/probatio/artefacts.py +201 -0
- probatio_llm-0.1.0/src/probatio/assertions/__init__.py +35 -0
- probatio_llm-0.1.0/src/probatio/assertions/backends.py +125 -0
- probatio_llm-0.1.0/src/probatio/assertions/contains.py +99 -0
- probatio_llm-0.1.0/src/probatio/assertions/judge.py +122 -0
- probatio_llm-0.1.0/src/probatio/assertions/result.py +44 -0
- probatio_llm-0.1.0/src/probatio/assertions/schema.py +144 -0
- probatio_llm-0.1.0/src/probatio/assertions/similarity.py +57 -0
- probatio_llm-0.1.0/src/probatio/budget.py +640 -0
- probatio_llm-0.1.0/src/probatio/case.py +374 -0
- probatio_llm-0.1.0/src/probatio/cassette.py +896 -0
- probatio_llm-0.1.0/src/probatio/cli.py +681 -0
- probatio_llm-0.1.0/src/probatio/collector.py +381 -0
- probatio_llm-0.1.0/src/probatio/errors.py +72 -0
- probatio_llm-0.1.0/src/probatio/hashing.py +95 -0
- probatio_llm-0.1.0/src/probatio/judge/__init__.py +56 -0
- probatio_llm-0.1.0/src/probatio/judge/core.py +221 -0
- probatio_llm-0.1.0/src/probatio/judge/kappa.py +92 -0
- probatio_llm-0.1.0/src/probatio/judge/prompt.py +91 -0
- probatio_llm-0.1.0/src/probatio/judge/rubric.py +122 -0
- probatio_llm-0.1.0/src/probatio/judge/validation.py +175 -0
- probatio_llm-0.1.0/src/probatio/metamorphic/__init__.py +109 -0
- probatio_llm-0.1.0/src/probatio/metamorphic/base.py +230 -0
- probatio_llm-0.1.0/src/probatio/metamorphic/evaluate.py +124 -0
- probatio_llm-0.1.0/src/probatio/metamorphic/freeze.py +278 -0
- probatio_llm-0.1.0/src/probatio/metamorphic/relations.py +658 -0
- probatio_llm-0.1.0/src/probatio/metamorphic/variants.py +219 -0
- probatio_llm-0.1.0/src/probatio/plugin.py +592 -0
- probatio_llm-0.1.0/src/probatio/providers/__init__.py +28 -0
- probatio_llm-0.1.0/src/probatio/providers/anthropic.py +172 -0
- probatio_llm-0.1.0/src/probatio/providers/base.py +68 -0
- probatio_llm-0.1.0/src/probatio/providers/claude_cli.py +287 -0
- probatio_llm-0.1.0/src/probatio/providers/fake.py +186 -0
- probatio_llm-0.1.0/src/probatio/py.typed +0 -0
- probatio_llm-0.1.0/src/probatio/reporters/__init__.py +37 -0
- probatio_llm-0.1.0/src/probatio/reporters/junit.py +219 -0
- probatio_llm-0.1.0/src/probatio/reporters/markdown.py +97 -0
- probatio_llm-0.1.0/src/probatio/reporters/results.py +66 -0
- probatio_llm-0.1.0/src/probatio/reporters/tables.py +196 -0
- probatio_llm-0.1.0/src/probatio/reporters/terminal.py +82 -0
- probatio_llm-0.1.0/src/probatio/runner.py +87 -0
- probatio_llm-0.1.0/src/probatio/session.py +490 -0
- probatio_llm-0.1.0/src/probatio/snapshot.py +515 -0
- probatio_llm-0.1.0/src/probatio/stability/__init__.py +40 -0
- probatio_llm-0.1.0/src/probatio/stability/stats.py +229 -0
- probatio_llm-0.1.0/src/probatio/stability/wilson.py +56 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Byte-compiled / cache
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Virtual environments
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
env/
|
|
10
|
+
|
|
11
|
+
# Build artefacts
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
.eggs/
|
|
16
|
+
|
|
17
|
+
# Tooling caches
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.hypothesis/
|
|
22
|
+
|
|
23
|
+
# Coverage
|
|
24
|
+
.coverage
|
|
25
|
+
.coverage.*
|
|
26
|
+
htmlcov/
|
|
27
|
+
coverage.xml
|
|
28
|
+
|
|
29
|
+
# Editors and OS
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
.DS_Store
|
|
33
|
+
|
|
34
|
+
# NOTE: .probatio/ is deliberately NOT ignored. It holds committed baselines and
|
|
35
|
+
# judge validation records, which CI must read.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to Probatio, newest first. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
The `Added` list below is in the order of the specification's architecture section, which is also
|
|
8
|
+
the order in which the phases recorded in [`PROGRESS.md`](PROGRESS.md) built it.
|
|
9
|
+
|
|
10
|
+
This file states no measurement. Every figure Probatio has produced about itself is printed in
|
|
11
|
+
[`README.md`](README.md) and under [`docs/`](docs/), and indexed against the committed file that
|
|
12
|
+
produced it in [`docs/PROVENANCE.md`](docs/PROVENANCE.md); repeating a number here would be a
|
|
13
|
+
second copy with no test behind it.
|
|
14
|
+
|
|
15
|
+
## [0.1.0] — 2026-09-06
|
|
16
|
+
|
|
17
|
+
First release. Probatio is a pytest plugin for regression-testing LLM applications, distributed as
|
|
18
|
+
`probatio-llm` and imported as `probatio`. Its two headline features are metamorphic relations and
|
|
19
|
+
flakiness statistics; everything else on this list exists so that those two are usable from a
|
|
20
|
+
normal test suite.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Foundations.** `ProbatioError` and the seven subclasses that name the offending case and, where
|
|
25
|
+
one exists, the command that fixes it: `ProbatioConfigError`, `MissingCassetteError`,
|
|
26
|
+
`StaleCassetteError`, `MissingVariantsError`, `BaselineDriftError`, `BudgetExceededError`,
|
|
27
|
+
`JudgeOutputError`. `stable_hash`, the canonical-JSON content hash behind every persisted
|
|
28
|
+
identity in the tool — cassette keys, prompt hashes, variant provenance, label files — stable
|
|
29
|
+
across processes and platforms.
|
|
30
|
+
- **Cases.** `LLMCase`, a pydantic model with an id, a string-or-mapping input, an optional system
|
|
31
|
+
prompt and parameters, its assertions, an optional budget, a snapshot mode and tags; and
|
|
32
|
+
`load_cases`, which loads a directory of YAML files in filename order and reports a malformed
|
|
33
|
+
file by name and field rather than by traceback.
|
|
34
|
+
- **Providers.** The `Provider` protocol and the `Completion` it returns, carrying text, token
|
|
35
|
+
counts, latency and a cost that is `None` when the provider cannot price the call rather than a
|
|
36
|
+
zero a ceiling would silently pass. Four adapters: `FakeProvider` and `ScriptedProvider` for
|
|
37
|
+
tests, `AnthropicProvider` over the SDK behind the `anthropic` extra, and `ClaudeCLIProvider`,
|
|
38
|
+
which runs the Claude Code CLI as a subprocess in a fresh empty directory so a developer on a
|
|
39
|
+
Claude plan can record without an API key. `--probatio-timeout` sets the subprocess ceiling.
|
|
40
|
+
- **Assertions.** Five checks over one `AssertionResult`: `schema_valid` (JSON Schema, with fenced
|
|
41
|
+
blocks stripped), `contains` and `not_contains`, `similarity` against a reference, and `judge`.
|
|
42
|
+
Similarity ships a pure-Python trigram-cosine backend and a `SimilarityBackend` protocol for
|
|
43
|
+
pluggable embeddings; nothing is downloaded, and no backend but the default is ever installed or
|
|
44
|
+
invoked by a test run.
|
|
45
|
+
- **Judge and validation.** `Judge` grades a case against a markdown rubric resolved from
|
|
46
|
+
`rubrics/`; the prompt template is a module constant whose hash is part of every judge cassette
|
|
47
|
+
key. A reply that is not strict JSON is asked again a bounded number of times and then returned
|
|
48
|
+
as a failed assertion, never raised. `cohens_kappa` and the validation record it writes;
|
|
49
|
+
`probatio validate-judge` measures a judge against human labels, from two existing columns or by
|
|
50
|
+
running the judge over the rows, and exits non-zero below `--min-kappa`. A judge with no
|
|
51
|
+
validation record on disk is reported as unenforceable rather than trusted.
|
|
52
|
+
- **Snapshots.** Per-case baselines under `.probatio/baseline/`, in `scores` or `output` mode,
|
|
53
|
+
keyed on a hash of the prompt so a case whose input changed is reported as a new baseline rather
|
|
54
|
+
than as drift. `--update-baseline` rewrites them; `--baseline-dir` moves them.
|
|
55
|
+
- **Budgets.** Per-case `max_cost_usd` and `max_latency_ms`, suite-wide `--max-cost` and
|
|
56
|
+
`--max-latency`, and `PriceTable` loaded from `--probatio-prices`. **The unenforceable rule:** a
|
|
57
|
+
cost ceiling evaluated where any call has an unknown cost is reported under warnings and not
|
|
58
|
+
under passed checks, because a ceiling that cannot be evaluated has not been met. No prices ship
|
|
59
|
+
with the package.
|
|
60
|
+
- **Cassettes.** `CassetteProvider` wraps any provider in `replay`, `record` or `off` mode, with
|
|
61
|
+
`replay` the default, so a suite runs from committed tapes and makes no model call. Tapes are
|
|
62
|
+
keyed on prompt, system, model, parameters and — for a judge call — the template hash, so an
|
|
63
|
+
edited prompt or rubric is detected as a stale tape rather than replayed as if nothing changed.
|
|
64
|
+
Missing and stale tapes are both raised and carried into the report. `--cassette-dir` moves the
|
|
65
|
+
store; `probatio import-cassettes` builds one from a JSONL of interactions recorded elsewhere.
|
|
66
|
+
- **Metamorphic relations.** The `Relation` base and its registry, and four decorators:
|
|
67
|
+
`@paraphrase_invariant`, `@order_invariant`, `@distractor_robust` and `@format_jitter`. Each
|
|
68
|
+
generates variants of a case that should not change its verdict; the plugin runs them and reports
|
|
69
|
+
a per-relation violation rate with the individual flips, and prints *not applicable* rather than
|
|
70
|
+
a rate for a relation whose case does not carry the field it needs. Paraphrases are not generated
|
|
71
|
+
at test time: `probatio freeze-variants` asks a model once, a human reviews and edits the
|
|
72
|
+
written files, and the suite reads the frozen, committed variants.
|
|
73
|
+
- **Flakiness statistics.** `--runs N` repeats each case in place and reports a per-case pass rate
|
|
74
|
+
with a hand-implemented Wilson 95% interval, a majority verdict, a suite stability score, and the
|
|
75
|
+
number of cases whose interval falls below their declared floor. `@flaky_tolerant(p=…)` declares
|
|
76
|
+
that floor per case; a case that declares none is held to a floor of 1.0.
|
|
77
|
+
- **Reporters.** One `RunReport` rendered four ways: the terminal section pytest prints at the end
|
|
78
|
+
of every run, a GitHub-flavoured markdown report written to `$GITHUB_STEP_SUMMARY` or
|
|
79
|
+
`--probatio-report`, JUnit XML at `--probatio-junit` carrying pass rates, interval bounds, costs
|
|
80
|
+
and per-relation violation rates as properties, and the full report as JSON at
|
|
81
|
+
`--probatio-results`. A quantity that was not measured is omitted, never written as a zero.
|
|
82
|
+
- **The pytest surface.** The `probatio`, `provider` and `judge_provider` fixtures; the
|
|
83
|
+
`probatio_relation` and `flaky_tolerant` markers; and the option group that selects the provider
|
|
84
|
+
and model, the judge provider and model, the cassette mode and store, the baseline directory,
|
|
85
|
+
the price table, the ceilings, the timeout and the three report paths. A non-fake provider with
|
|
86
|
+
no model named is refused before the session starts.
|
|
87
|
+
- **The `probatio` console script.** `validate-judge`, `freeze-variants` and `import-cassettes`.
|
|
88
|
+
These are the only entry points that call a live model, and only when a human runs them.
|
|
89
|
+
- **Packaging.** `probatio-llm` on Python 3.11 and newer, with runtime dependencies of exactly
|
|
90
|
+
`pytest`, `pydantic`, `jsonschema` and `PyYAML`; an `anthropic` extra for the SDK adapter and a
|
|
91
|
+
`dev` extra for the gate. Ships `py.typed`: the package is typed and `mypy --strict` is part of
|
|
92
|
+
the gate. The sdist carries the package, this file, the README and the licence, and neither
|
|
93
|
+
`tests/` nor `examples/`.
|
|
94
|
+
- **Examples.** `examples/demo_suite/`, the executable design specification, written before the
|
|
95
|
+
implementation and byte-frozen since — every decorator, every assertion type and a deliberate
|
|
96
|
+
failing case, runnable offline. `examples/consilium/`, a dogfood suite over a real
|
|
97
|
+
retrieval-augmented medical Q&A system, offline from committed tapes, with a live variant used
|
|
98
|
+
to record them.
|
|
99
|
+
- **Documentation.** `README.md` with a prior-art comparison against DeepEval, promptfoo, Giskard,
|
|
100
|
+
Ragas, Braintrust and LangSmith; `docs/DESIGN.md`, `docs/assertions.md`, `docs/relations.md`,
|
|
101
|
+
`docs/stability.md`, `docs/providers.md`, `docs/EVALUATION.md`, `docs/CASE_STUDY.md`; and
|
|
102
|
+
`docs/PROVENANCE.md`, which indexes every number in the documentation to the committed file that
|
|
103
|
+
produced it and is enforced by a test that re-derives each one.
|
|
104
|
+
- **Continuous integration.** The five-part quality gate — pytest, coverage of `src/probatio`,
|
|
105
|
+
ruff, `mypy --strict`, and byte-identity of the frozen demo suite — on the floor version and the
|
|
106
|
+
one above it, with no credentials and no network access to a model provider.
|
|
107
|
+
|
|
108
|
+
### Known limitations
|
|
109
|
+
|
|
110
|
+
Stated in full, with the evidence, in [`docs/CASE_STUDY.md`](docs/CASE_STUDY.md) §5 and
|
|
111
|
+
[`docs/EVALUATION.md`](docs/EVALUATION.md); summarised here because they bound what this release
|
|
112
|
+
has shown.
|
|
113
|
+
|
|
114
|
+
- **The judge in this repository is not validated on disk, deliberately.** Its two measured kappas
|
|
115
|
+
disagree with each other, and neither is high; `docs/EVALUATION.md` §4 gives both and says why no
|
|
116
|
+
validation record was written. Every judge verdict in the case study therefore carries
|
|
117
|
+
`unenforceable=True`, and the relation rates that depend on those verdicts inherit it. They are
|
|
118
|
+
reported because an unenforceable result is still a measurement — of an unvalidated instrument.
|
|
119
|
+
- **A suite cannot yet declare a minimum kappa that fails the build.**
|
|
120
|
+
`probatio validate-judge --min-kappa` exits non-zero from the command line, but there is no
|
|
121
|
+
equivalent of a budget overrun inside a pytest run.
|
|
122
|
+
- **`--runs N` repeats inside one pytest item, so function-scoped fixtures are shared across the
|
|
123
|
+
runs.** That is deliberate — it measures the model's nondeterminism, not state leakage — but a
|
|
124
|
+
suite wanting a fresh fixture per run has to build one inside its own callable
|
|
125
|
+
(`docs/stability.md`).
|
|
126
|
+
- **The case study's live system under test is not the system it borrows its questions from.** It
|
|
127
|
+
makes one grounded call with the corpus notes already in hand; the real pipeline plans,
|
|
128
|
+
retrieves, runs agents and repairs. Retrieval is not modelled at all, so nothing here speaks to
|
|
129
|
+
retrieval quality.
|
|
130
|
+
- **The case study is a small sample, run once, on one model family.** Fifteen cases, one run
|
|
131
|
+
each, two Anthropic models reached through the same adapter. No interval in that document is a
|
|
132
|
+
confidence interval, and the flip pattern most in need of repetition is the one repetition would
|
|
133
|
+
have resolved.
|
|
134
|
+
- **Route A of the case study is retrospective.** The regression it replays was found and
|
|
135
|
+
documented by the system's own evaluation before the suite was written, and the suite's
|
|
136
|
+
assertions were derived from that system's golden labels and phrase list.
|
|
137
|
+
- **Costs reported through `ClaudeCLIProvider` are notional.** They are the API price the CLI
|
|
138
|
+
computes for calls made on a subscription that was not billed that money;
|
|
139
|
+
`docs/providers.md` says so wherever the figure is used.
|
|
140
|
+
- **Anthropic is the only live provider.** There is no OpenAI adapter and none is planned; another
|
|
141
|
+
provider is a one-class extension, documented in `docs/providers.md`.
|
|
142
|
+
- **Similarity's default backend is lexical.** Trigram cosine is deterministic and needs no
|
|
143
|
+
download, which is why it is the default; it is not a semantic measure, and
|
|
144
|
+
`docs/assertions.md` publishes the band it produced on real answers instead of recommending a
|
|
145
|
+
threshold.
|
|
146
|
+
|
|
147
|
+
[0.1.0]: https://github.com/Lexieli666/probatio/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yichen Li
|
|
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.
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: probatio-llm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pytest plugin for regression-testing LLM applications: metamorphic relations and flakiness statistics.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Lexieli666/probatio
|
|
6
|
+
Project-URL: Repository, https://github.com/Lexieli666/probatio
|
|
7
|
+
Project-URL: Changelog, https://github.com/Lexieli666/probatio/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: Yichen Li <lexieli@seas.upenn.edu>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: llm,metamorphic-testing,pytest,regression-testing,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: jsonschema>=4
|
|
22
|
+
Requires-Dist: pydantic>=2
|
|
23
|
+
Requires-Dist: pytest>=8
|
|
24
|
+
Requires-Dist: pyyaml>=6
|
|
25
|
+
Provides-Extra: anthropic
|
|
26
|
+
Requires-Dist: anthropic>=0.40; extra == 'anthropic'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build; extra == 'dev'
|
|
29
|
+
Requires-Dist: coverage; extra == 'dev'
|
|
30
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine; extra == 'dev'
|
|
33
|
+
Requires-Dist: types-jsonschema; extra == 'dev'
|
|
34
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Probatio
|
|
38
|
+
|
|
39
|
+
Probatio is a **pytest plugin for regression-testing LLM applications**. You write cases in YAML,
|
|
40
|
+
one test function per suite, and keep running the suite you already have. It adds two disciplines
|
|
41
|
+
the software-testing literature has and LLM developer tooling mostly does not: **metamorphic
|
|
42
|
+
relations**, decorators that assert a semantics-preserving change to an input does not change the
|
|
43
|
+
verdict, reported as a per-relation violation rate; and **flakiness statistics**, repeated
|
|
44
|
+
execution with a per-case pass rate, a Wilson 95% interval and a suite stability score, instead of
|
|
45
|
+
running a nondeterministic system once and printing a tick. Everything else it ships — JSON-schema
|
|
46
|
+
validity, contains and not-contains assertions, similarity, a rubric judge with Cohen's κ against
|
|
47
|
+
human labels, snapshot baselines, cost and latency ceilings, record-replay cassettes, and
|
|
48
|
+
terminal, GitHub-summary, JUnit-XML and JSON reporters — is there so those two are usable on a
|
|
49
|
+
real suite. The judge shipped here has **measured agreement** with human labels, not validated
|
|
50
|
+
agreement; §*Case study* says what was measured and what it came to.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install probatio-llm
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
# examples/demo_suite/cases/06-copd-spirometry.yaml
|
|
60
|
+
id: copd-spirometry
|
|
61
|
+
input: "In one sentence, what test confirms a diagnosis of COPD?"
|
|
62
|
+
assertions:
|
|
63
|
+
- {type: contains, any: ["spirometry"]}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# examples/demo_suite/test_demo.py
|
|
68
|
+
CASES = load_cases(HERE / "cases")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("case", CASES, ids=lambda c: c.id)
|
|
72
|
+
@format_jitter(field="input.question")
|
|
73
|
+
def test_case(case, probatio, provider):
|
|
74
|
+
probatio.check(case, sut=lambda c: answer(c, provider))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pytest examples/demo_suite --runs 5
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That is shortened from [`examples/demo_suite/`](examples/demo_suite/), the executable
|
|
82
|
+
specification this tool was built against and which has not been edited since it was written. Every
|
|
83
|
+
line above is verbatim from it except one: the real test parametrizes over a filtered subset of
|
|
84
|
+
`CASES` rather than over `CASES` itself, because the demo routes its expected-fail and flaky cases
|
|
85
|
+
to their own test functions. What is left out is the rest of that case — a second assertion, a
|
|
86
|
+
budget, its tags — and the three other relations the real `test_case` carries. Note what the shown
|
|
87
|
+
case does *not* exercise: its `input` is a bare string, so `@format_jitter(field="input.question")`
|
|
88
|
+
reports **not applicable** on it rather than `0.00`, and
|
|
89
|
+
[`examples/demo_suite/cases/01-htn-definition.yaml`](examples/demo_suite/cases/01-htn-definition.yaml)
|
|
90
|
+
is the dict-input case, with a `question` and a list of `documents`, that the field relations
|
|
91
|
+
actually apply to.
|
|
92
|
+
|
|
93
|
+
## Metamorphic relations
|
|
94
|
+
|
|
95
|
+
A relation names a transformation that should preserve a case's meaning and asserts the verdict
|
|
96
|
+
does not move. Four ship as decorators — `@order_invariant`, `@paraphrase_invariant`,
|
|
97
|
+
`@distractor_robust`, `@format_jitter` — and each reports a violation rate **alongside** the case,
|
|
98
|
+
never folded into its pass or fail. A violation counts in either direction: a variant that passes
|
|
99
|
+
where the original failed breaks the invariant just as much.
|
|
100
|
+
|
|
101
|
+
From `pytest examples/demo_suite --runs 5`, run at commit `4c2114e` and recorded verbatim in
|
|
102
|
+
[`PROGRESS.md`](PROGRESS.md):
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
relations:
|
|
106
|
+
relation cases n/a violations mean rate worst case worst rate
|
|
107
|
+
-------------------- ----- --- ---------- --------- ------------------- ----------
|
|
108
|
+
distractor_robust 7 1 0/70 0.00 gerd-alarm-features 0.00
|
|
109
|
+
format_jitter 7 1 15/105 0.14 htn-definition 0.33
|
|
110
|
+
order_invariant 6 2 0/50 0.00 gerd-alarm-features 0.00
|
|
111
|
+
paraphrase_invariant 2 0 5/30 0.17 htn-definition 0.33
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `n/a` column is the point of the design: a relation a case is out of scope for — reordering a
|
|
115
|
+
one-element document list, rewording a case whose input is a bare string — reports *not
|
|
116
|
+
applicable*, never `0.00`. Paraphrases are generated once by `probatio freeze-variants`, reviewed
|
|
117
|
+
by a human and committed; they are never sampled during a test run.
|
|
118
|
+
|
|
119
|
+
## Flakiness statistics
|
|
120
|
+
|
|
121
|
+
`--runs N` runs each case N times and reports what happened, rather than what happened once. Same
|
|
122
|
+
run, same commit:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
cases:
|
|
126
|
+
case verdict assertions pass rate 95% Wilson floor cost latency ms snapshot
|
|
127
|
+
--------------------- ------- ---------- --------- ------------ ----- --------- ---------- ---------
|
|
128
|
+
htn-definition pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
|
|
129
|
+
htn-first-line pass 3/3 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
|
|
130
|
+
t2d-screening-json pass 2/2 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
|
|
131
|
+
t2d-metformin pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
|
|
132
|
+
gerd-alarm-features pass 3/3 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
|
|
133
|
+
copd-spirometry pass 2/2 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
|
|
134
|
+
red-flag-chest-pain pass 3/3 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
|
|
135
|
+
insomnia-first-line pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 -
|
|
136
|
+
htn-definition pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
|
|
137
|
+
t2d-metformin pass 4/4 1.00 [0.57, 1.00] 1.00 $0.000500 100 unchanged
|
|
138
|
+
anxiety-expected-fail FAIL 1/3 0.00 [0.00, 0.43] 1.00 $0.000500 100 -
|
|
139
|
+
flu-antivirals-flaky pass 2/2 0.80 [0.38, 0.96] 0.80 $0.000500 100 -
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
stability score: 0.90 over 12 repeated case(s)
|
|
144
|
+
cases whose Wilson lower bound is below their floor: 12 of 12
|
|
145
|
+
cost: $0.031500 (no --max-cost ceiling)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`flu-antivirals-flaky` declares `@flaky_tolerant(p=0.8, n=5)`, passes four of its five runs, and
|
|
149
|
+
passes the case at the bar its author wrote down; `anxiety-expected-fail` fails every run and is
|
|
150
|
+
in the suite so the report has a failure to show. The stability score is the mean of those twelve
|
|
151
|
+
pass rates.
|
|
152
|
+
|
|
153
|
+
**"12 of 12" is not twelve problems.** It counts the repeated cases whose Wilson *lower* bound
|
|
154
|
+
falls under their floor, and at n = 5 the lower bound of a perfect five-for-five is 0.57, so no
|
|
155
|
+
case can clear a floor of 1.0 however well it does — the line is a distance from the suite's own
|
|
156
|
+
floors, and it becomes informative when `--runs` is large enough for a bound to reach one
|
|
157
|
+
([`docs/stability.md`](docs/stability.md)). Nothing in it decides a verdict: the observed pass
|
|
158
|
+
rate does that.
|
|
159
|
+
|
|
160
|
+
The run also names what it could not check:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
warnings (7):
|
|
164
|
+
- htn-definition: 1 judge verdict(s) from a rubric with no validation record (run: probatio validate-judge --labels <csv> --rubric <name>)
|
|
165
|
+
- htn-first-line: 1 judge verdict(s) from a rubric with no validation record (run: probatio validate-judge --labels <csv> --rubric <name>)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
— and five more like them, one per judged case. A judge with no validation record on disk is not a
|
|
169
|
+
validated judge, and the tool says so about every suite, including its own.
|
|
170
|
+
|
|
171
|
+
## Prior art
|
|
172
|
+
|
|
173
|
+
The field is crowded and this table is the honest version of where Probatio sits. Every claim in
|
|
174
|
+
it is what that tool's own documentation says, **checked against each tool's documentation on
|
|
175
|
+
2026-09-05**; "not documented" means the page that would carry the feature was read and does not
|
|
176
|
+
carry it, not that the feature is absent.
|
|
177
|
+
|
|
178
|
+
| tool | what it is (their words) | pytest | repeated runs | interval on a per-case pass rate | metamorphic relations | JUnit XML |
|
|
179
|
+
|---|---|---|---|---|---|---|
|
|
180
|
+
| **DeepEval** | "plugs into Pytest, so `deepeval test run` collects and runs your eval files the same way pytest would"; a large catalog of "research-backed LLM-as-a-Judge" metrics | yes, native (`assert_test`) | not documented | none documented; a **flaky metric** flag instead | none documented | not documented |
|
|
181
|
+
| **promptfoo** | "open-source CLI and library for evaluating and red-teaming LLM apps" | no | **yes**, `--repeat <number>` | none documented | none documented | **yes**, `--output junit.xml` |
|
|
182
|
+
| **Giskard** | "enterprise platform and open-source library for LLM evaluation and security" | yes; behavioural tests "pass or fail under pytest" | not documented | none documented | the word does not appear; its scan is adversarial generation | not documented |
|
|
183
|
+
| **Ragas** | "move from 'vibe checks' to systematic evaluation loops"; RAG metrics | not documented | not documented | none documented | none documented | not documented |
|
|
184
|
+
| **Braintrust** | "the active observability platform for instrumenting, understanding, and improving agents" | not documented | not documented | none documented | none documented | not documented |
|
|
185
|
+
| **LangSmith** | "a framework for measuring quality throughout the application lifecycle" | **yes**, `@pytest.mark.langsmith` | **yes**, `num_repetitions` | mean and standard deviation; no interval documented | none documented | not documented |
|
|
186
|
+
| **Probatio** | this repository | yes, a plugin | yes, `--runs N` | Wilson 95% per case, plus a suite stability score and a floor marker | four, as decorators, with frozen variants | yes, with the statistics as `<properties>` |
|
|
187
|
+
|
|
188
|
+
Pages read on 2026-09-05: `deepeval.com/docs/getting-started` and `/docs/metrics-introduction`;
|
|
189
|
+
`promptfoo.dev/docs/intro` and `/docs/usage/command-line`; `docs.giskard.ai`;
|
|
190
|
+
`docs.ragas.io/en/stable`; `braintrust.dev/docs` and `/docs/guides/evals`;
|
|
191
|
+
`docs.langchain.com/langsmith/evaluation-concepts`, `/langsmith/pytest`, `/langsmith/repetition`
|
|
192
|
+
and `/langsmith/evaluate-llm-application`.
|
|
193
|
+
|
|
194
|
+
**What is not a differentiator, said plainly.** Being pytest-native is not one: DeepEval, Giskard
|
|
195
|
+
and LangSmith all are, and DeepEval owns that ground with a far larger metric catalog. Running a
|
|
196
|
+
case repeatedly is not one: promptfoo has `--repeat` and LangSmith has `num_repetitions`. JUnit
|
|
197
|
+
output is not one: promptfoo writes `junit.xml`. If you want a metric catalog, use DeepEval; if
|
|
198
|
+
you want a red-teaming engine, use promptfoo or Giskard; if you want hosted eval tracing, use
|
|
199
|
+
Braintrust or LangSmith. Probatio is meant to sit beside them.
|
|
200
|
+
|
|
201
|
+
**What is.** A confidence interval on a per-case pass rate, a suite-level stability score, and a
|
|
202
|
+
floor marker (`@flaky_tolerant(p, n)`) that holds a case to a number somebody wrote down — none of
|
|
203
|
+
the six documents any of these. And metamorphic relations as stated invariants, declared as
|
|
204
|
+
decorators on the test: none of the six documents those either, and Giskard's scan is adversarial
|
|
205
|
+
generation, which searches for inputs that break a system rather than checking a property the
|
|
206
|
+
system is supposed to have. The one-sentence contrast that matters: **DeepEval's answer to a flaky
|
|
207
|
+
metric is a flag that makes its failure non-deciding; Probatio's is to run the case n times and
|
|
208
|
+
report the rate, the interval and the floor.**
|
|
209
|
+
|
|
210
|
+
Metamorphic testing of LLMs exists as research, and Probatio claims no new relation — its four are
|
|
211
|
+
packaging, with reviewable frozen variants, of transformations these papers name. LLMorph
|
|
212
|
+
`[LLMORPH]` and MTF `[MTF]` are the two research frameworks in this space; the NLP catalogue
|
|
213
|
+
`[MR-CATALOG-NLP]` collected **191** metamorphic relations in a literature review, of which 36
|
|
214
|
+
were implemented, and it is the source `order_invariant` and `paraphrase_invariant` cite. Chen et
|
|
215
|
+
al. `[CHEN-MT-SURVEY]` and Segura et al. `[SEGURA-MT-SURVEY]` are the surveys for the definition
|
|
216
|
+
of a relation and for violation-as-oracle. Full entries, with authors, venues, years and DOIs
|
|
217
|
+
verified against Crossref on 2026-09-05, are in [`docs/relations.md`](docs/relations.md).
|
|
218
|
+
|
|
219
|
+
## Case study
|
|
220
|
+
|
|
221
|
+
Probatio was dogfooded on
|
|
222
|
+
[Consilium-Health](https://github.com/Lexieli666/consilium-health), the author's own
|
|
223
|
+
public multi-agent retrieval-grounded
|
|
224
|
+
question-answering project, along two routes. Both are written up with their limitations in
|
|
225
|
+
[`docs/CASE_STUDY.md`](docs/CASE_STUDY.md), and every number below is re-derived from a committed
|
|
226
|
+
artefact by `tests/test_docs_case_study.py`.
|
|
227
|
+
|
|
228
|
+
**Route A — a retrospective catch, and the headline claim.** Fifteen of Consilium's golden items
|
|
229
|
+
replay through Probatio from its published traces, against two recorded configurations. On
|
|
230
|
+
`baseline_llm`, a single call with no retrieval and no agents, all fifteen pass. On the
|
|
231
|
+
multi-agent `full` pipeline, **five of the six red-flag cases fail** — each on a `contains`
|
|
232
|
+
assertion carrying Consilium's own thirty-eight escalation phrases, and on nothing else — while
|
|
233
|
+
**none fails on `baseline_llm`**. That is a safety regression Consilium had already documented in
|
|
234
|
+
its own `docs/FAILURE_CASES.md`, reproduced as a failing `pytest` run from committed tapes with no
|
|
235
|
+
model called. It is labelled retrospective in the case study, because it is: the suite was written
|
|
236
|
+
after the regression was known.
|
|
237
|
+
|
|
238
|
+
**Route B — prospective in form.** One flag changed, `claude-opus-5` to
|
|
239
|
+
`claude-haiku-4-5-20251001`, on a live single-call system under test, with nothing else moved.
|
|
240
|
+
The suite reported that **4 of 15 verdicts moved**, all of them on the judge; that the judge marked
|
|
241
|
+
**a third of the cases** (5 of 15) unfaithful to their sources, naming the sentence in each; that
|
|
242
|
+
eight snapshots drifted, three of them on cases whose every assertion still passed; that
|
|
243
|
+
`paraphrase_invariant` went from 0.04 to 0.30 under rewordings a human had confirmed do not change
|
|
244
|
+
the question; and that the cheaper recording cost **$0.987356 against $6.037457** in notional API
|
|
245
|
+
price, a six-fold difference. It is prospective in form — suite first, change second, outcome
|
|
246
|
+
unknown in advance — and it is on a system under test built for the purpose, not on a shipping
|
|
247
|
+
application. The case study says so in those words.
|
|
248
|
+
|
|
249
|
+
Dogfooding also found three things wrong with Probatio itself — two defects and one gap that nine
|
|
250
|
+
phases of unit tests had not reached — all fixed during Phase 12 and listed with the commit that
|
|
251
|
+
fixed each in [`docs/EVALUATION.md`](docs/EVALUATION.md) §7.
|
|
252
|
+
|
|
253
|
+
## Design positions
|
|
254
|
+
|
|
255
|
+
**Similarity is for paraphrase-stable content; a judge is for claims.** Reach for `similarity`
|
|
256
|
+
when what you are protecting is wording that should stay roughly the same, and for a `judge` when
|
|
257
|
+
it is whether a claim is true, grounded or complete. The docs take that position rather than
|
|
258
|
+
listing options, because the substitution fails in a way a threshold cannot fix: a trigram cosine
|
|
259
|
+
scores a sentence that *contradicts* the reference at 0.223 while the same claim in different
|
|
260
|
+
words scores 0.453, so the ordering is wrong, not the number
|
|
261
|
+
([`docs/assertions.md`](docs/assertions.md), rationale in [`docs/DESIGN.md`](docs/DESIGN.md)).
|
|
262
|
+
|
|
263
|
+
**An unenforceable ceiling is never a pass.** A `max_cost_usd` ceiling evaluated where any call's
|
|
264
|
+
cost is unknown cannot be checked, so it is reported as unenforceable, listed under warnings, and
|
|
265
|
+
excluded from passing checks — never quietly satisfied by treating unknown as zero. The same holds
|
|
266
|
+
for a case that recorded no provider calls at all: a check that cannot fail is not a check. This
|
|
267
|
+
is the failure mode a testing tool must not have, and it is why `Completion.cost_usd` is `None`
|
|
268
|
+
rather than `0.0` when nobody priced the call (`docs/DESIGN.md`, Phases 6 and 11).
|
|
269
|
+
|
|
270
|
+
**Frozen variants.** Paraphrases are generated once by `probatio freeze-variants`, written to
|
|
271
|
+
`variants/<case_id>.yaml` with their provenance, reviewed, and committed. Three reasons in order:
|
|
272
|
+
the suite stays deterministic; the model is not part of the oracle at test time; and the variants
|
|
273
|
+
are in the diff, so a reviewer can throw out a rewording that changed the meaning. That last one is
|
|
274
|
+
not hypothetical — **2 of 45** frozen paraphrases were deleted at review in this project, each
|
|
275
|
+
noted in its file's header ([`docs/EVALUATION.md`](docs/EVALUATION.md) §3).
|
|
276
|
+
|
|
277
|
+
**A plugin, not a platform.** Teams do not migrate eval platforms mid-quarter; they add a
|
|
278
|
+
dependency to the tests they already run. So Probatio is four runtime dependencies, one entry
|
|
279
|
+
point, a `Provider` protocol of one method, and reporters that write into CI you already have —
|
|
280
|
+
JUnit XML and a GitHub job summary — rather than a dashboard to log into. No hosted service, no
|
|
281
|
+
async, no metric catalog (`docs/DESIGN.md`, Phase 0).
|
|
282
|
+
|
|
283
|
+
## Install
|
|
284
|
+
|
|
285
|
+
The distribution is named `probatio-llm`; the import name is `probatio`.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
pip install probatio-llm # core: pytest, pydantic, jsonschema, PyYAML
|
|
289
|
+
pip install "probatio-llm[anthropic]" # plus the Anthropic SDK adapter
|
|
290
|
+
pip install "probatio-llm[dev]" # ruff, mypy, coverage, build, twine
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Python 3.11 or newer. The `claude-cli` provider needs no extra and no API key — it runs the Claude
|
|
294
|
+
Code CLI as a subprocess, so a developer on a Claude plan can record cassettes and freeze variants;
|
|
295
|
+
the cost it reports is *notional* API price, not money billed, and
|
|
296
|
+
[`docs/providers.md`](docs/providers.md) says so wherever that figure is used.
|
|
297
|
+
|
|
298
|
+
> The unrelated PyPI placeholder distribution `probatio` also installs a `probatio/` import
|
|
299
|
+
> package. Do not install both into the same environment.
|
|
300
|
+
|
|
301
|
+
## Status and roadmap
|
|
302
|
+
|
|
303
|
+
Pre-1.0. Phase-by-phase state, with the gate result and the run log for each phase, is in
|
|
304
|
+
[`PROGRESS.md`](PROGRESS.md); every judgement call made without asking is numbered in
|
|
305
|
+
[`DECISIONS.md`](DECISIONS.md); anything abandoned is in [`BLOCKERS.md`](BLOCKERS.md). Every number
|
|
306
|
+
in this file and under `docs/` is indexed in [`docs/PROVENANCE.md`](docs/PROVENANCE.md) with the
|
|
307
|
+
committed file it came from and the command that regenerates it.
|
|
308
|
+
|
|
309
|
+
Next, in the order they would be built:
|
|
310
|
+
|
|
311
|
+
- **Per-run fixture isolation.** `--runs N` repeats inside one pytest item, so function-scoped
|
|
312
|
+
fixtures are shared across the runs. That is deliberate — it measures the model's
|
|
313
|
+
nondeterminism, not state leakage — but a suite that wants a fresh fixture per run has to build
|
|
314
|
+
it inside its own callable today (`docs/stability.md`).
|
|
315
|
+
- **A minimum-κ gate.** `probatio validate-judge --min-kappa` already exits non-zero, but a suite
|
|
316
|
+
cannot yet declare a floor that fails the build the way a budget overrun does. The judge in this
|
|
317
|
+
repository scored κ 0.600 on one blind forty-row sample and 0.253 on another, which is the
|
|
318
|
+
argument for the gate and for reporting the number rather than the adjective.
|
|
319
|
+
- **The variance study.** The `--runs` engine is also an instrument: run a benchmark subset n
|
|
320
|
+
times and measure how often rankings flip. Seeded as an optional phase in `PROGRESS.md`, and
|
|
321
|
+
the one thing that would settle whether the judge or the answer is the unstable half of this
|
|
322
|
+
project's own relation rates (`docs/EVALUATION.md` §2).
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
MIT. See [`LICENSE`](LICENSE).
|