evalseal 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.
- evalseal-0.1.0/.env.example +4 -0
- evalseal-0.1.0/.github/workflows/ci.yml +33 -0
- evalseal-0.1.0/.github/workflows/release.yml +52 -0
- evalseal-0.1.0/.gitignore +223 -0
- evalseal-0.1.0/DESIGN.md +76 -0
- evalseal-0.1.0/LICENSE +21 -0
- evalseal-0.1.0/PKG-INFO +116 -0
- evalseal-0.1.0/README.md +86 -0
- evalseal-0.1.0/examples/borderline_judge/dataset.jsonl +20 -0
- evalseal-0.1.0/examples/borderline_judge/scorer.json +6 -0
- evalseal-0.1.0/examples/borderline_judge/target.json +1 -0
- evalseal-0.1.0/pyproject.toml +48 -0
- evalseal-0.1.0/src/evalseal/__init__.py +3 -0
- evalseal-0.1.0/src/evalseal/adapters/__init__.py +0 -0
- evalseal-0.1.0/src/evalseal/adapters/dataset.py +36 -0
- evalseal-0.1.0/src/evalseal/adapters/recording.py +56 -0
- evalseal-0.1.0/src/evalseal/adapters/scorer.py +74 -0
- evalseal-0.1.0/src/evalseal/adapters/target.py +97 -0
- evalseal-0.1.0/src/evalseal/analyze.py +82 -0
- evalseal-0.1.0/src/evalseal/cli.py +152 -0
- evalseal-0.1.0/src/evalseal/executor.py +142 -0
- evalseal-0.1.0/src/evalseal/ledger.py +60 -0
- evalseal-0.1.0/src/evalseal/models.py +78 -0
- evalseal-0.1.0/src/evalseal/report.py +48 -0
- evalseal-0.1.0/tests/cassettes/run.json +4424 -0
- evalseal-0.1.0/tests/conftest.py +25 -0
- evalseal-0.1.0/tests/test_analyze.py +45 -0
- evalseal-0.1.0/tests/test_cli.py +124 -0
- evalseal-0.1.0/tests/test_executor.py +53 -0
- evalseal-0.1.0/tests/test_ledger.py +92 -0
- evalseal-0.1.0/tests/test_provenance_gap.py +84 -0
- evalseal-0.1.0/tests/test_recording.py +43 -0
- evalseal-0.1.0/tests/test_verify.py +75 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
strategy:
|
|
7
|
+
matrix:
|
|
8
|
+
python-version: ["3.11", "3.12"]
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with: { python-version: "${{ matrix.python-version }}", cache: pip }
|
|
13
|
+
- run: pip install -e ".[dev]"
|
|
14
|
+
- name: Tests (no API key — cassettes replay)
|
|
15
|
+
run: pytest -q --cov=evalseal
|
|
16
|
+
- name: Demo run reproduces from cassette
|
|
17
|
+
run: |
|
|
18
|
+
if [ ! -f tests/cassettes/run.json ]; then
|
|
19
|
+
echo "::warning::tests/cassettes/run.json not recorded yet; skipping demo replay."
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
set +e
|
|
23
|
+
evalseal run \
|
|
24
|
+
--dataset examples/borderline_judge/dataset.jsonl \
|
|
25
|
+
--target-config examples/borderline_judge/target.json \
|
|
26
|
+
--scorer-config examples/borderline_judge/scorer.json \
|
|
27
|
+
--n 5 --cassette tests/cassettes/run.json
|
|
28
|
+
code=$?
|
|
29
|
+
# 0 = all stable, 3 = unstable cases found (expected for this demo).
|
|
30
|
+
# Anything else means the replay itself broke.
|
|
31
|
+
if [ "$code" -ne 0 ] && [ "$code" -ne 3 ]; then exit "$code"; fi
|
|
32
|
+
- name: Ledger integrity
|
|
33
|
+
run: evalseal verify
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with: { python-version: "3.12", cache: pip }
|
|
13
|
+
- name: Tag matches package version
|
|
14
|
+
run: |
|
|
15
|
+
version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
16
|
+
if [ "${GITHUB_REF_NAME#v}" != "$version" ]; then
|
|
17
|
+
echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $version"; exit 1
|
|
18
|
+
fi
|
|
19
|
+
- run: pip install -e ".[dev]" build twine
|
|
20
|
+
- name: Tests (no API key — cassettes replay)
|
|
21
|
+
run: pytest -q
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- run: twine check dist/*
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with: { name: dist, path: dist/ }
|
|
26
|
+
|
|
27
|
+
publish-pypi:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
# Trusted publishing: PyPI verifies this workflow via OIDC. No token is stored.
|
|
31
|
+
environment:
|
|
32
|
+
name: pypi
|
|
33
|
+
url: https://pypi.org/project/evalseal/
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with: { name: dist, path: dist/ }
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
40
|
+
|
|
41
|
+
github-release:
|
|
42
|
+
needs: publish-pypi
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
permissions:
|
|
45
|
+
contents: write
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/download-artifact@v4
|
|
48
|
+
with: { name: dist, path: dist/ }
|
|
49
|
+
- name: Create GitHub release
|
|
50
|
+
env:
|
|
51
|
+
GH_TOKEN: ${{ github.token }}
|
|
52
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
|
|
220
|
+
# EvalSeal local state and generated reports
|
|
221
|
+
.evalseal/
|
|
222
|
+
report.json
|
|
223
|
+
report.md
|
evalseal-0.1.0/DESIGN.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# EvalSeal design
|
|
2
|
+
|
|
3
|
+
## What it claims, and what it doesn't
|
|
4
|
+
|
|
5
|
+
**EvalSeal measures reproducibility. It does not create determinism.** Setting a
|
|
6
|
+
temperature or seed does not make a hosted model deterministic, and EvalSeal never says it
|
|
7
|
+
does. It runs every case N times and reports how often the verdict disagrees with itself.
|
|
8
|
+
|
|
9
|
+
**A green CI replay proves the harness is reproducible, not that the model is.** Replaying
|
|
10
|
+
a cassette gives back the recorded responses, so the analysis, scores and report can be
|
|
11
|
+
regenerated from them exactly. It says nothing about whether the live model would answer
|
|
12
|
+
the same way today. To learn that, record again and `diff` the runs.
|
|
13
|
+
|
|
14
|
+
**The ledger is tamper-evident, not tamper-proof.** Each record's SHA-256 covers its whole
|
|
15
|
+
content plus the previous record's hash. Editing a past score breaks that record's hash,
|
|
16
|
+
and re-hashing it breaks the next record's link. Anyone who can rewrite the *whole* file
|
|
17
|
+
can build a new valid chain, though. Anchoring the head hash somewhere external
|
|
18
|
+
(signing, notarization) is out of scope for v0.
|
|
19
|
+
|
|
20
|
+
## Components
|
|
21
|
+
|
|
22
|
+
| module | role |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `analyze.py` | Pure functions: seeded bootstrap CI, flip rate, stability class. No I/O. |
|
|
25
|
+
| `adapters/recording.py` | Cassette keyed by the SHA-256 of the effective request body. |
|
|
26
|
+
| `adapters/target.py` | `Target` protocol; `LocalCallableTarget` (tests), `OpenAICompatibleTarget`. |
|
|
27
|
+
| `adapters/scorer.py` | Exact, regex, and LLM-judge scorers. The judge is a `Target`. |
|
|
28
|
+
| `executor.py` | N-run loop, provenance capture, provenance-gap warnings. |
|
|
29
|
+
| `ledger.py` | Hash-linked append-only JSONL; `verify_chain`. |
|
|
30
|
+
| `report.py`, `cli.py` | `report.md` / `report.json`; `run`, `verify`, `diff`. |
|
|
31
|
+
|
|
32
|
+
## Decisions
|
|
33
|
+
|
|
34
|
+
**The cassette stores responses as ordered lists.** Repeats of a case send identical
|
|
35
|
+
requests. If the cassette kept one response per request hash, repeat #1 would be replayed
|
|
36
|
+
N times and every recorded flip would disappear. Instead, the k-th identical call gets the
|
|
37
|
+
k-th recorded response. This depends on calls happening in the same order in record and
|
|
38
|
+
replay mode, which holds because the executor is sequential.
|
|
39
|
+
|
|
40
|
+
**Only the effective request is hashed.** The cassette key is the URL plus the JSON body
|
|
41
|
+
actually sent. Unset parameters are left out of the body entirely, so "temperature not
|
|
42
|
+
set" and "temperature=1.0" are different requests. API keys never enter the key or the
|
|
43
|
+
cassette.
|
|
44
|
+
|
|
45
|
+
**Provenance is checked on every response.** A served-model mismatch on call 37 matters
|
|
46
|
+
even when call 100 looks clean. The manifest records the first response's metadata, and
|
|
47
|
+
drift warnings flag any change in served model or system fingerprint during the run.
|
|
48
|
+
|
|
49
|
+
**Dated snapshots are not mismatches.** Requesting `gpt-4o-mini` and being served
|
|
50
|
+
`gpt-4o-mini-2024-07-18` is the provider resolving an alias. The snapshot is still stored
|
|
51
|
+
in `served_model`. Any other difference produces a warning.
|
|
52
|
+
|
|
53
|
+
**Judge verdict parsing is strict.** The first standalone `PASS`/`FAIL` token decides, and
|
|
54
|
+
anything else counts as FAIL. A plain substring check would read "FAIL — does not PASS" as
|
|
55
|
+
a pass.
|
|
56
|
+
|
|
57
|
+
**Exit codes separate outcomes.** `run` exits 3 when any case is UNSTABLE, which is
|
|
58
|
+
different from 1 (error) and 2 (usage). CI can then accept an expected unstable demo while
|
|
59
|
+
still failing on a broken replay.
|
|
60
|
+
|
|
61
|
+
## Known limits
|
|
62
|
+
|
|
63
|
+
- **Small-N bootstrap CIs are approximate.** With N=5 binary verdicts the bootstrap
|
|
64
|
+
distribution is coarse (steps of 0.2) and tends to under-cover. The CI's seed is fixed,
|
|
65
|
+
so the interval itself is reproducible, but it is still only an approximation.
|
|
66
|
+
- **Flip rate needs N ≥ 5 to mean much.** At N=3 a single dissent already reads as 33%.
|
|
67
|
+
Use a larger N on the cases you care about.
|
|
68
|
+
- **Stability thresholds are conventions.** `BORDERLINE_MAX_FLIP = 0.20` is a named
|
|
69
|
+
constant chosen for auditability, not a statistically derived cutoff.
|
|
70
|
+
- **The `diff` noise floor is conservative.** It uses the widest per-case CI half-width
|
|
71
|
+
across both runs. That rarely claims a false "REAL CHANGE", but it will miss small real
|
|
72
|
+
shifts in the aggregate. A paired test across cases would be more powerful.
|
|
73
|
+
- **Float scores use a median-crossing pseudo-flip.** This gives a variance signal
|
|
74
|
+
without a threshold, but it is a heuristic. All built-in scorers are binary.
|
|
75
|
+
- **Canonical hosts are an allowlist.** Any self-hosted or gateway endpoint gets a
|
|
76
|
+
NON-CANONICAL warning by design. The warning means "provenance unverified", not "wrong".
|
evalseal-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Venkata Manideep Patibandla
|
|
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.
|
evalseal-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: evalseal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reproducibility and provenance receipts for LLM evaluations.
|
|
5
|
+
Project-URL: Homepage, https://github.com/patibandlavenkatamanideep/evalseal
|
|
6
|
+
Project-URL: Issues, https://github.com/patibandlavenkatamanideep/evalseal/issues
|
|
7
|
+
Author: Venkata Manideep Patibandla
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: evaluation,llm,llm-as-judge,provenance,reproducibility
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic>=2.6
|
|
24
|
+
Requires-Dist: rich>=13.7
|
|
25
|
+
Requires-Dist: typer>=0.12
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# EvalSeal
|
|
32
|
+
|
|
33
|
+
**Reproducibility receipts for LLM evals: run it N times, report the score with its noise, and seal what actually ran.**
|
|
34
|
+
|
|
35
|
+
## The problem
|
|
36
|
+
|
|
37
|
+
An eval score from a single run is one sample of a random process. Run the same eval again
|
|
38
|
+
and borderline items quietly flip from PASS to FAIL, especially when an LLM judge grades
|
|
39
|
+
them. Reports also name the model you *asked* for, not the one that *answered*. EvalSeal
|
|
40
|
+
measures the flips, records the real provenance, and seals both into a tamper-evident ledger.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install evalseal
|
|
46
|
+
|
|
47
|
+
# The recorded demo (examples + cassette) lives in the repo.
|
|
48
|
+
git clone https://github.com/patibandlavenkatamanideep/evalseal && cd evalseal
|
|
49
|
+
|
|
50
|
+
# Replays the committed cassette: no API key, no network.
|
|
51
|
+
evalseal run \
|
|
52
|
+
--dataset examples/borderline_judge/dataset.jsonl \
|
|
53
|
+
--target-config examples/borderline_judge/target.json \
|
|
54
|
+
--scorer-config examples/borderline_judge/scorer.json \
|
|
55
|
+
--n 5
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Real flip rates
|
|
59
|
+
|
|
60
|
+
Recorded 2026-09-15 and committed in `tests/cassettes/run.json`: `gemini-2.5-flash` as both
|
|
61
|
+
the target and the judge, temperature left at the provider default, 20 arguable prompts,
|
|
62
|
+
5 runs each. The quickstart above replays exactly this run.
|
|
63
|
+
|
|
64
|
+
**Mean score: 0.92, but 5 of 20 cases did not get the same verdict every time.**
|
|
65
|
+
|
|
66
|
+
| case | prompt | verdicts | mean | 95% CI | flip rate | stability |
|
|
67
|
+
|---|---|---|---|---|---|---|
|
|
68
|
+
| b01 | Is a hot dog a sandwich? | `FPPFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
|
|
69
|
+
| b10 | Blockchain for a child in exactly 20 words | `FPFPP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
|
|
70
|
+
| b16 | "Do we only use 10% of our brains?" in a jokey tone | `PPFFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
|
|
71
|
+
| b05 | A borderline-polite refusal to a coworker | `PFPPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
|
|
72
|
+
| b19 | A technically accurate haiku about recursion | `PPFPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
|
|
73
|
+
| 15 others | | `PPPPP` | 1.00 | [1.00, 1.00] | 0% | STABLE |
|
|
74
|
+
|
|
75
|
+
Treat the k-th repeat of every case as one ordinary single-run eval, and the five
|
|
76
|
+
"single runs" of this identical eval scored **0.90, 0.95, 0.85, 0.90 and 1.00**. A single
|
|
77
|
+
run can't tell you which of those numbers you got.
|
|
78
|
+
|
|
79
|
+
The report also flagged `TEMPERATURE NOT SET` for both the target and the judge, which is
|
|
80
|
+
the reason these borderline verdicts can come out differently from run to run.
|
|
81
|
+
|
|
82
|
+
To re-record with your own key, copy `.env.example` to `.env`, add a free
|
|
83
|
+
[Google AI Studio](https://aistudio.google.com/apikey) key, and run the quickstart with
|
|
84
|
+
`EVALSEAL_RECORD=1`. If the free tier rate-limits you, run the same command again later;
|
|
85
|
+
responses already recorded are kept.
|
|
86
|
+
|
|
87
|
+
## Commands
|
|
88
|
+
|
|
89
|
+
| command | what it does | exit code |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `evalseal run` | Runs each case N times, analyzes variance, seals a record, writes `report.json` + `report.md`. | `0` all stable/borderline · `3` any case UNSTABLE · `1` error |
|
|
92
|
+
| `evalseal verify` | Recomputes every hash in `.evalseal/ledger.jsonl` and checks the chain links. | `0` intact · `1` tampered or broken |
|
|
93
|
+
| `evalseal diff A B` | Compares two ledger runs and says whether the mean moved beyond the noise floor. Use `--` for negative indices: `evalseal diff -- 0 -1`. | `0` |
|
|
94
|
+
|
|
95
|
+
**Stability classes** are based on the flip rate, the share of a case's N verdicts that
|
|
96
|
+
disagree with its majority: `STABLE` (0), `BORDERLINE` (≤ 20%), `UNSTABLE` (> 20%).
|
|
97
|
+
|
|
98
|
+
**Provenance warnings** show up in the report when:
|
|
99
|
+
- the served model differs from the requested one (for the target or the judge),
|
|
100
|
+
- the endpoint isn't a canonical provider host,
|
|
101
|
+
- temperature was left at the provider default,
|
|
102
|
+
- the served model or system fingerprint changed partway through the run.
|
|
103
|
+
|
|
104
|
+
## How it works
|
|
105
|
+
|
|
106
|
+
The executor sends each prompt to the target N times and scores every response. An LLM
|
|
107
|
+
judge is itself a target, so its own randomness is measured instead of assumed away.
|
|
108
|
+
`analyze.py` computes the mean, a seeded bootstrap 95% CI, and the flip rate for each case.
|
|
109
|
+
Every request goes through a cassette. In record mode, real responses are saved in call
|
|
110
|
+
order; in replay mode, which is the default and what CI uses, they are served back, and a
|
|
111
|
+
missing entry fails loudly. Each run is saved as a `RunRecord`: its manifest (requested vs.
|
|
112
|
+
served model, fingerprint, parameters and whether they were set explicitly, rubric hash,
|
|
113
|
+
dataset hash) plus its results. The record is hashed and linked to the previous record's
|
|
114
|
+
hash in an append-only JSONL ledger, so editing any past score breaks `verify`.
|
|
115
|
+
|
|
116
|
+
See [DESIGN.md](https://github.com/patibandlavenkatamanideep/evalseal/blob/main/DESIGN.md) for what this does and does not prove.
|
evalseal-0.1.0/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# EvalSeal
|
|
2
|
+
|
|
3
|
+
**Reproducibility receipts for LLM evals: run it N times, report the score with its noise, and seal what actually ran.**
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
An eval score from a single run is one sample of a random process. Run the same eval again
|
|
8
|
+
and borderline items quietly flip from PASS to FAIL, especially when an LLM judge grades
|
|
9
|
+
them. Reports also name the model you *asked* for, not the one that *answered*. EvalSeal
|
|
10
|
+
measures the flips, records the real provenance, and seals both into a tamper-evident ledger.
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install evalseal
|
|
16
|
+
|
|
17
|
+
# The recorded demo (examples + cassette) lives in the repo.
|
|
18
|
+
git clone https://github.com/patibandlavenkatamanideep/evalseal && cd evalseal
|
|
19
|
+
|
|
20
|
+
# Replays the committed cassette: no API key, no network.
|
|
21
|
+
evalseal run \
|
|
22
|
+
--dataset examples/borderline_judge/dataset.jsonl \
|
|
23
|
+
--target-config examples/borderline_judge/target.json \
|
|
24
|
+
--scorer-config examples/borderline_judge/scorer.json \
|
|
25
|
+
--n 5
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Real flip rates
|
|
29
|
+
|
|
30
|
+
Recorded 2026-09-15 and committed in `tests/cassettes/run.json`: `gemini-2.5-flash` as both
|
|
31
|
+
the target and the judge, temperature left at the provider default, 20 arguable prompts,
|
|
32
|
+
5 runs each. The quickstart above replays exactly this run.
|
|
33
|
+
|
|
34
|
+
**Mean score: 0.92, but 5 of 20 cases did not get the same verdict every time.**
|
|
35
|
+
|
|
36
|
+
| case | prompt | verdicts | mean | 95% CI | flip rate | stability |
|
|
37
|
+
|---|---|---|---|---|---|---|
|
|
38
|
+
| b01 | Is a hot dog a sandwich? | `FPPFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
|
|
39
|
+
| b10 | Blockchain for a child in exactly 20 words | `FPFPP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
|
|
40
|
+
| b16 | "Do we only use 10% of our brains?" in a jokey tone | `PPFFP` | 0.60 | [0.20, 1.00] | 40% | UNSTABLE |
|
|
41
|
+
| b05 | A borderline-polite refusal to a coworker | `PFPPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
|
|
42
|
+
| b19 | A technically accurate haiku about recursion | `PPFPP` | 0.80 | [0.40, 1.00] | 20% | BORDERLINE |
|
|
43
|
+
| 15 others | | `PPPPP` | 1.00 | [1.00, 1.00] | 0% | STABLE |
|
|
44
|
+
|
|
45
|
+
Treat the k-th repeat of every case as one ordinary single-run eval, and the five
|
|
46
|
+
"single runs" of this identical eval scored **0.90, 0.95, 0.85, 0.90 and 1.00**. A single
|
|
47
|
+
run can't tell you which of those numbers you got.
|
|
48
|
+
|
|
49
|
+
The report also flagged `TEMPERATURE NOT SET` for both the target and the judge, which is
|
|
50
|
+
the reason these borderline verdicts can come out differently from run to run.
|
|
51
|
+
|
|
52
|
+
To re-record with your own key, copy `.env.example` to `.env`, add a free
|
|
53
|
+
[Google AI Studio](https://aistudio.google.com/apikey) key, and run the quickstart with
|
|
54
|
+
`EVALSEAL_RECORD=1`. If the free tier rate-limits you, run the same command again later;
|
|
55
|
+
responses already recorded are kept.
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| command | what it does | exit code |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `evalseal run` | Runs each case N times, analyzes variance, seals a record, writes `report.json` + `report.md`. | `0` all stable/borderline · `3` any case UNSTABLE · `1` error |
|
|
62
|
+
| `evalseal verify` | Recomputes every hash in `.evalseal/ledger.jsonl` and checks the chain links. | `0` intact · `1` tampered or broken |
|
|
63
|
+
| `evalseal diff A B` | Compares two ledger runs and says whether the mean moved beyond the noise floor. Use `--` for negative indices: `evalseal diff -- 0 -1`. | `0` |
|
|
64
|
+
|
|
65
|
+
**Stability classes** are based on the flip rate, the share of a case's N verdicts that
|
|
66
|
+
disagree with its majority: `STABLE` (0), `BORDERLINE` (≤ 20%), `UNSTABLE` (> 20%).
|
|
67
|
+
|
|
68
|
+
**Provenance warnings** show up in the report when:
|
|
69
|
+
- the served model differs from the requested one (for the target or the judge),
|
|
70
|
+
- the endpoint isn't a canonical provider host,
|
|
71
|
+
- temperature was left at the provider default,
|
|
72
|
+
- the served model or system fingerprint changed partway through the run.
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
The executor sends each prompt to the target N times and scores every response. An LLM
|
|
77
|
+
judge is itself a target, so its own randomness is measured instead of assumed away.
|
|
78
|
+
`analyze.py` computes the mean, a seeded bootstrap 95% CI, and the flip rate for each case.
|
|
79
|
+
Every request goes through a cassette. In record mode, real responses are saved in call
|
|
80
|
+
order; in replay mode, which is the default and what CI uses, they are served back, and a
|
|
81
|
+
missing entry fails loudly. Each run is saved as a `RunRecord`: its manifest (requested vs.
|
|
82
|
+
served model, fingerprint, parameters and whether they were set explicitly, rubric hash,
|
|
83
|
+
dataset hash) plus its results. The record is hashed and linked to the previous record's
|
|
84
|
+
hash in an append-only JSONL ledger, so editing any past score breaks `verify`.
|
|
85
|
+
|
|
86
|
+
See [DESIGN.md](https://github.com/patibandlavenkatamanideep/evalseal/blob/main/DESIGN.md) for what this does and does not prove.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{"case_id": "b01", "prompt": "Is a hot dog a sandwich? Answer yes or no with one sentence.", "expected": null}
|
|
2
|
+
{"case_id": "b02", "prompt": "Summarize the causes of WWI in exactly one sentence.", "expected": null}
|
|
3
|
+
{"case_id": "b03", "prompt": "Give a 'mostly correct' explanation of how vaccines work in 2 sentences.", "expected": null}
|
|
4
|
+
{"case_id": "b04", "prompt": "Answer ambiguously whether Pluto is a planet.", "expected": null}
|
|
5
|
+
{"case_id": "b05", "prompt": "Write a borderline-polite refusal to a coworker's request.", "expected": null}
|
|
6
|
+
{"case_id": "b06", "prompt": "Explain in one sentence why the sky is blue, without using the word 'scattering'.", "expected": null}
|
|
7
|
+
{"case_id": "b07", "prompt": "Is a tomato a vegetable? Answer in one sentence.", "expected": null}
|
|
8
|
+
{"case_id": "b08", "prompt": "Name the largest desert in the world in one word.", "expected": null}
|
|
9
|
+
{"case_id": "b09", "prompt": "In one sentence, was Napoleon short?", "expected": null}
|
|
10
|
+
{"case_id": "b10", "prompt": "Describe how a blockchain works to a child in exactly 20 words.", "expected": null}
|
|
11
|
+
{"case_id": "b11", "prompt": "What is the capital of Australia? Include one fun fact, keep it under 15 words.", "expected": null}
|
|
12
|
+
{"case_id": "b12", "prompt": "Summarize the plot of Hamlet in exactly two sentences.", "expected": null}
|
|
13
|
+
{"case_id": "b13", "prompt": "Is zero an even number? Explain briefly, but hedge a little.", "expected": null}
|
|
14
|
+
{"case_id": "b14", "prompt": "How many continents are there? One sentence.", "expected": null}
|
|
15
|
+
{"case_id": "b15", "prompt": "Give a one-sentence definition of 'machine learning' that a statistician would accept.", "expected": null}
|
|
16
|
+
{"case_id": "b16", "prompt": "Do humans only use 10% of their brains? Answer in a casual, jokey tone in one sentence.", "expected": null}
|
|
17
|
+
{"case_id": "b17", "prompt": "Translate 'break a leg' into French, keeping the idiomatic meaning, in one line.", "expected": null}
|
|
18
|
+
{"case_id": "b18", "prompt": "Is glass a liquid? Give a short answer with one caveat.", "expected": null}
|
|
19
|
+
{"case_id": "b19", "prompt": "Write a haiku about recursion that is technically accurate.", "expected": null}
|
|
20
|
+
{"case_id": "b20", "prompt": "In one sentence, who invented the light bulb?", "expected": null}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "llm_judge",
|
|
3
|
+
"judge_model": "gemini-2.5-flash",
|
|
4
|
+
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
5
|
+
"rubric": "You are a strict grader. PASS only if the response is factually correct, directly answers the prompt, and is well-formed. Otherwise FAIL."
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "model": "gemini-2.5-flash", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai" }
|