prompt-eval-harness 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prompt_eval_harness-0.2.0/LICENSE +21 -0
- prompt_eval_harness-0.2.0/PKG-INFO +275 -0
- prompt_eval_harness-0.2.0/README.md +250 -0
- prompt_eval_harness-0.2.0/pyproject.toml +40 -0
- prompt_eval_harness-0.2.0/setup.cfg +4 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/__init__.py +30 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/__main__.py +6 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/cli.py +205 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/judge.py +49 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/models.py +180 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/promptfoo.py +103 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/providers.py +132 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/reports.py +86 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/runio.py +100 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/runner.py +95 -0
- prompt_eval_harness-0.2.0/src/prompt_eval/scorers.py +196 -0
- prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/PKG-INFO +275 -0
- prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/SOURCES.txt +26 -0
- prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/dependency_links.txt +1 -0
- prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/entry_points.txt +2 -0
- prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/requires.txt +5 -0
- prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/top_level.txt +1 -0
- prompt_eval_harness-0.2.0/tests/test_cli.py +60 -0
- prompt_eval_harness-0.2.0/tests/test_new_features.py +166 -0
- prompt_eval_harness-0.2.0/tests/test_prompt_regression_example.py +44 -0
- prompt_eval_harness-0.2.0/tests/test_providers.py +55 -0
- prompt_eval_harness-0.2.0/tests/test_runner_reports.py +47 -0
- prompt_eval_harness-0.2.0/tests/test_scorers.py +80 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 prompt-eval-harness contributors
|
|
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,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prompt-eval-harness
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Deterministic, offline-first prompt and agent evaluation regression harness (library + CLI + GitHub Action)
|
|
5
|
+
Author: prompt-eval-harness contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AsherLucas/prompt-eval-harness
|
|
8
|
+
Project-URL: Repository, https://github.com/AsherLucas/prompt-eval-harness
|
|
9
|
+
Project-URL: Issues, https://github.com/AsherLucas/prompt-eval-harness/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: PyYAML>=6.0
|
|
21
|
+
Requires-Dist: jsonschema>=4.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# prompt-eval-harness
|
|
27
|
+
|
|
28
|
+
[](https://github.com/AsherLucas/prompt-eval-harness/actions/workflows/ci.yml)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
[](pyproject.toml)
|
|
31
|
+
|
|
32
|
+
A deterministic, offline-first prompt and agent evaluation regression
|
|
33
|
+
harness: a Python library, a CLI, and a GitHub Action that fail CI the
|
|
34
|
+
way a broken unit test does when a prompt, model, or retrieval change
|
|
35
|
+
silently degrades output.
|
|
36
|
+
|
|
37
|
+
> **Honest status (v0.2.0): this is an early-stage tool.** It has
|
|
38
|
+
> no users yet, and it does **not yet qualify for any six-month
|
|
39
|
+
> maintainer programme** (such as OpenAI Codex for Open Source or
|
|
40
|
+
> Anthropic Claude for Open Source, which reward already
|
|
41
|
+
> widely-used open-source projects). No stars, downloads, users,
|
|
42
|
+
> contributors, or benchmark results are claimed anywhere in this
|
|
43
|
+
> repository, because none exist yet. Adoption, if it comes, has
|
|
44
|
+
> to be earned by other developers actually using it.
|
|
45
|
+
|
|
46
|
+
## 5-minute quickstart
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Use a virtual environment (required on externally-managed systems
|
|
50
|
+
# such as Debian/Ubuntu, where system pip refuses package installs):
|
|
51
|
+
python3 -m venv .venv
|
|
52
|
+
.venv/bin/pip install -e ".[dev]"
|
|
53
|
+
# Commands below use .venv/bin/python so they work without activating
|
|
54
|
+
# the venv; after `source .venv/bin/activate`, plain `python` works too.
|
|
55
|
+
|
|
56
|
+
# Offline demo — no API key, no network, all 30 golden cases pass:
|
|
57
|
+
.venv/bin/python -m prompt_eval run --cases examples/cases.yaml --provider stub
|
|
58
|
+
|
|
59
|
+
# The same run, with reports:
|
|
60
|
+
.venv/bin/python -m prompt_eval run --cases examples/cases.yaml --provider stub \
|
|
61
|
+
--markdown report.md --junit report.xml
|
|
62
|
+
|
|
63
|
+
# Watch the CI gate fail on purpose (1/3 passed, exit code 1):
|
|
64
|
+
.venv/bin/python -m prompt_eval run --cases examples/failing.yaml --provider stub; echo "exit=$?"
|
|
65
|
+
|
|
66
|
+
# Catch a prompt regression with run-diff (see
|
|
67
|
+
# examples/prompt-regression/README.md for the verified output):
|
|
68
|
+
.venv/bin/python -m prompt_eval run --cases examples/prompt-regression/cases.yaml \
|
|
69
|
+
--provider stub --prompt-version v1 --json /tmp/baseline.json
|
|
70
|
+
.venv/bin/python -m prompt_eval run --cases examples/prompt-regression/regressed.yaml \
|
|
71
|
+
--provider stub --prompt-version v2 --json /tmp/new.json
|
|
72
|
+
.venv/bin/python -m prompt_eval diff --baseline /tmp/baseline.json \
|
|
73
|
+
--new /tmp/new.json; echo "diff exit=$?"
|
|
74
|
+
# (The same baseline is also checked in as
|
|
75
|
+
# examples/prompt-regression/baseline.json.)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Verified in a clean venv against this checkout: golden cases
|
|
79
|
+
`30/30 passed`, failing example `1/3 passed` with exit code 1,
|
|
80
|
+
prompt-regression example `6/6` baseline vs `5/6` regressed with
|
|
81
|
+
exactly one `Pass -> Fail` case (`refund-window`), diff exit 1.
|
|
82
|
+
|
|
83
|
+
Use it as a library, too:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from prompt_eval import Case, Expectation, run_cases
|
|
87
|
+
from prompt_eval.providers import StubProvider
|
|
88
|
+
|
|
89
|
+
cases = [Case(id="greeting", prompt="Say hello",
|
|
90
|
+
expected=[Expectation(type="exact", params={"value": "Hello!"})])]
|
|
91
|
+
report = run_cases(cases, StubProvider({"greeting": "Hello!"}))
|
|
92
|
+
print(report.passed_count, "/", report.total, "gate:", report.gate_passed)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Case file format
|
|
96
|
+
|
|
97
|
+
A YAML file with a `cases` list. Each case has `prompt`, optional
|
|
98
|
+
`inputs` (substituted into `{placeholders}` in the prompt), an
|
|
99
|
+
`expected` list, and — for offline demos only — an optional
|
|
100
|
+
`stub_output`:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
version: 1
|
|
104
|
+
cases:
|
|
105
|
+
- id: json-person
|
|
106
|
+
prompt: "Return {name} as JSON"
|
|
107
|
+
inputs: {name: Ada}
|
|
108
|
+
expected:
|
|
109
|
+
- type: json_schema
|
|
110
|
+
schema:
|
|
111
|
+
type: object
|
|
112
|
+
required: [name, age]
|
|
113
|
+
properties:
|
|
114
|
+
name: {type: string}
|
|
115
|
+
age: {type: integer, minimum: 0}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Scorer catalogue
|
|
119
|
+
|
|
120
|
+
All scorers are deterministic and offline. None of them calls a model.
|
|
121
|
+
|
|
122
|
+
| Scorer | What it checks | Key parameters |
|
|
123
|
+
| ------ | -------------- | -------------- |
|
|
124
|
+
| `exact` | Output equals a value | `value`, `case_sensitive` (default true), `strip` (default true) |
|
|
125
|
+
| `regex` | Output matches a pattern | `pattern`, `ignore_case`, `multiline` |
|
|
126
|
+
| `json_schema` | Output parses as JSON and matches a full JSON Schema (via `jsonschema`) | `schema` |
|
|
127
|
+
| `length` | Character / word bounds | `min_length`, `max_length`, `min_words`, `max_words` |
|
|
128
|
+
| `format` | Output is well-formed | `kind`: `json`, `email`, `url`, `lowercase`, `uppercase` |
|
|
129
|
+
| `contains_all` | Output contains every listed string | `strings`, `case_sensitive` (default true) |
|
|
130
|
+
| `citation` | RAG-style output cites sources | `min_citations` (default 1), `pattern`, `required_sources` |
|
|
131
|
+
|
|
132
|
+
**JSON Schema (v0.2.0):** full validation via the `jsonschema`
|
|
133
|
+
package (Draft 2020-12), including `allOf`/`anyOf`/`oneOf`/`not`,
|
|
134
|
+
nested schemas, and format-independent keywords. An invalid schema
|
|
135
|
+
fails that expectation with a clear message instead of passing.
|
|
136
|
+
|
|
137
|
+
**Custom scorers:** register one in code with
|
|
138
|
+
`register_scorer(name, fn)`, or ship it via the
|
|
139
|
+
`prompt_eval.scorers` entry-point group. See
|
|
140
|
+
`examples/plugin_example.py` for a worked example.
|
|
141
|
+
|
|
142
|
+
**Citation default:** a citation is `[1]`, `[2]`, … or
|
|
143
|
+
`[source: name]`. Pass `pattern` to use a different marker style.
|
|
144
|
+
|
|
145
|
+
**LLM-as-judge (advisory only, v0.2.0):** add `--judge` to a run
|
|
146
|
+
to also ask a model to judge each output. Judge results are
|
|
147
|
+
reported in their own Markdown section, are never counted toward
|
|
148
|
+
pass rate or exit code, and a judge failure never fails the gate.
|
|
149
|
+
A non-deterministic judge cannot be a reproducible gate, so it
|
|
150
|
+
must stay advisory.
|
|
151
|
+
|
|
152
|
+
## Providers
|
|
153
|
+
|
|
154
|
+
- `stub` — returns each case's `stub_output`, or a placeholder
|
|
155
|
+
response. Offline, free, and the right choice for demos, tests,
|
|
156
|
+
and CI smoke runs. It proves the harness works; it does **not**
|
|
157
|
+
prove a model is good.
|
|
158
|
+
- `openai` — any OpenAI-compatible `/chat/completions` endpoint.
|
|
159
|
+
Configure with flags, never with a key in a file:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
export OPENAI_API_KEY="..." # from your provider, never commit it
|
|
163
|
+
python -m prompt_eval run --cases my-cases.yaml --provider openai \
|
|
164
|
+
--model gpt-4o-mini --prompt-version v3
|
|
165
|
+
# A different endpoint / key variable:
|
|
166
|
+
python -m prompt_eval run --cases my-cases.yaml --provider openai \
|
|
167
|
+
--base-url https://my-host.example/v1 --api-key-env MY_API_KEY_ENV
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Keys are read from the environment at run time and are never logged.
|
|
171
|
+
If the key variable is unset, the run fails with a clear error
|
|
172
|
+
instead of silently passing or failing cases. Transient provider
|
|
173
|
+
errors (HTTP 429/5xx, network errors) are retried with backoff
|
|
174
|
+
(`--retries`, default 2); other HTTP errors fail immediately.
|
|
175
|
+
|
|
176
|
+
- `ollama` — a local OpenAI-compatible server, default
|
|
177
|
+
`http://localhost:11434/v1`, no API key required:
|
|
178
|
+
`python -m prompt_eval run --cases my-cases.yaml --provider ollama --model llama3.1`
|
|
179
|
+
|
|
180
|
+
## Diff, record/replay, and Promptfoo import
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Compare two machine-readable runs (exit 1 if any case flipped pass -> fail):
|
|
184
|
+
python -m prompt_eval run --cases examples/cases.yaml --provider stub --json run-baseline.json
|
|
185
|
+
python -m prompt_eval run --cases examples/cases.yaml --provider stub --json run-new.json
|
|
186
|
+
python -m prompt_eval diff --baseline run-baseline.json --new run-new.json
|
|
187
|
+
|
|
188
|
+
# Record real outputs once, re-score them offline in CI (no model call):
|
|
189
|
+
python -m prompt_eval record --cases examples/cases.yaml --provider openai --recording rec.json
|
|
190
|
+
python -m prompt_eval replay --cases examples/cases.yaml --recording rec.json
|
|
191
|
+
|
|
192
|
+
# Import a basic Promptfoo file (deterministic assertions only; model
|
|
193
|
+
# assertions raise an error instead of being silently dropped):
|
|
194
|
+
python -m prompt_eval import-promptfoo --input promptfoo.yaml --output cases.yaml
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Token usage reported by the provider (prompt/completion/total
|
|
198
|
+
tokens) is shown in the Markdown report; cost is never estimated
|
|
199
|
+
from invented prices.
|
|
200
|
+
|
|
201
|
+
Worked example: `examples/prompt-regression/` is a realistic
|
|
202
|
+
6-case help-desk regression (baseline 6/6, one simulated
|
|
203
|
+
regression, diff names exactly that case) with its verified
|
|
204
|
+
output documented in its README.
|
|
205
|
+
|
|
206
|
+
## Reports and the CI gate
|
|
207
|
+
|
|
208
|
+
- `--markdown report.md` — summary table, gate result, run metadata
|
|
209
|
+
(provider/model, prompt version, harness version), and a section
|
|
210
|
+
per failed case.
|
|
211
|
+
- `--junit report.xml` — JUnit XML, so CI systems and GitHub code
|
|
212
|
+
scanning tooling can display failures per case.
|
|
213
|
+
- Exit code `0` means the gate passed (pass rate >= `--threshold`,
|
|
214
|
+
default `1.0`, i.e. every case must pass). Exit code `1` means a
|
|
215
|
+
case failed, the threshold was breached, or the provider failed.
|
|
216
|
+
Exit code `2` means a usage/config error (bad file, duplicate
|
|
217
|
+
case id, threshold out of range).
|
|
218
|
+
- `--threshold 0.9` allows a 90% pass rate; use it consciously, and
|
|
219
|
+
prefer `1.0` for a regression gate — a threshold below 1.0 lets
|
|
220
|
+
regressions through by design.
|
|
221
|
+
|
|
222
|
+
## CI guide
|
|
223
|
+
|
|
224
|
+
This repo dogfoods itself: `.github/workflows/ci.yml` runs the
|
|
225
|
+
pytest suite and the harness on its own golden cases on every push
|
|
226
|
+
and pull request (Python 3.11 and 3.12). For your own repo, copy
|
|
227
|
+
the template below, or use the bundled `action.yml` (see below).
|
|
228
|
+
The essential shape is:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
name: prompt-eval
|
|
232
|
+
on: [push, pull_request]
|
|
233
|
+
jobs:
|
|
234
|
+
eval:
|
|
235
|
+
runs-on: ubuntu-latest
|
|
236
|
+
steps:
|
|
237
|
+
- uses: actions/checkout@v4
|
|
238
|
+
- uses: actions/setup-python@v5
|
|
239
|
+
with: {python-version: "3.12"}
|
|
240
|
+
- run: pip install .
|
|
241
|
+
- run: >-
|
|
242
|
+
python -m prompt_eval run
|
|
243
|
+
--cases examples/cases.yaml --provider stub
|
|
244
|
+
--markdown report.md --junit report.xml
|
|
245
|
+
- uses: actions/upload-artifact@v4
|
|
246
|
+
if: always()
|
|
247
|
+
with: {name: prompt-eval-reports, path: "report.*"}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The bundled `action.yml` is a composite action wrapping the same
|
|
251
|
+
steps, so a consumer repo can call it with `cases`, `provider`,
|
|
252
|
+
`threshold`, and report-path inputs (see `action.yml`).
|
|
253
|
+
|
|
254
|
+
## Publishing (prepared, not yet published)
|
|
255
|
+
|
|
256
|
+
`.github/workflows/publish.yml` publishes to PyPI on version tags
|
|
257
|
+
via Trusted Publishing (OIDC, no token stored in the repo), and
|
|
258
|
+
`action.yml` already has Marketplace branding. The remaining steps
|
|
259
|
+
are account-owner steps only and are **not** done by this code:
|
|
260
|
+
create a PyPI account and enable 2FA, register a pending Trusted
|
|
261
|
+
Publisher for this repo/workflow on PyPI, and accept the GitHub
|
|
262
|
+
Marketplace Developer Agreement before publishing the Action.
|
|
263
|
+
|
|
264
|
+
## What v0.2.0 deliberately leaves out
|
|
265
|
+
|
|
266
|
+
- Hosted dashboard, SaaS, accounts, or any server component.
|
|
267
|
+
- Adapters for every LLM framework (LangChain, LangGraph,
|
|
268
|
+
LlamaIndex, …), every language, or a visual prompt editor.
|
|
269
|
+
- Training, fine-tuning, dataset generation, or a prompt
|
|
270
|
+
marketplace.
|
|
271
|
+
- A TypeScript port (a separate package later).
|
|
272
|
+
|
|
273
|
+
See [CHANGELOG.md](CHANGELOG.md) for the v0.2.0 scope,
|
|
274
|
+
[docs/user-guide.md](docs/user-guide.md) for the full guide, and
|
|
275
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for how to add a scorer.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# prompt-eval-harness
|
|
2
|
+
|
|
3
|
+
[](https://github.com/AsherLucas/prompt-eval-harness/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](pyproject.toml)
|
|
6
|
+
|
|
7
|
+
A deterministic, offline-first prompt and agent evaluation regression
|
|
8
|
+
harness: a Python library, a CLI, and a GitHub Action that fail CI the
|
|
9
|
+
way a broken unit test does when a prompt, model, or retrieval change
|
|
10
|
+
silently degrades output.
|
|
11
|
+
|
|
12
|
+
> **Honest status (v0.2.0): this is an early-stage tool.** It has
|
|
13
|
+
> no users yet, and it does **not yet qualify for any six-month
|
|
14
|
+
> maintainer programme** (such as OpenAI Codex for Open Source or
|
|
15
|
+
> Anthropic Claude for Open Source, which reward already
|
|
16
|
+
> widely-used open-source projects). No stars, downloads, users,
|
|
17
|
+
> contributors, or benchmark results are claimed anywhere in this
|
|
18
|
+
> repository, because none exist yet. Adoption, if it comes, has
|
|
19
|
+
> to be earned by other developers actually using it.
|
|
20
|
+
|
|
21
|
+
## 5-minute quickstart
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Use a virtual environment (required on externally-managed systems
|
|
25
|
+
# such as Debian/Ubuntu, where system pip refuses package installs):
|
|
26
|
+
python3 -m venv .venv
|
|
27
|
+
.venv/bin/pip install -e ".[dev]"
|
|
28
|
+
# Commands below use .venv/bin/python so they work without activating
|
|
29
|
+
# the venv; after `source .venv/bin/activate`, plain `python` works too.
|
|
30
|
+
|
|
31
|
+
# Offline demo — no API key, no network, all 30 golden cases pass:
|
|
32
|
+
.venv/bin/python -m prompt_eval run --cases examples/cases.yaml --provider stub
|
|
33
|
+
|
|
34
|
+
# The same run, with reports:
|
|
35
|
+
.venv/bin/python -m prompt_eval run --cases examples/cases.yaml --provider stub \
|
|
36
|
+
--markdown report.md --junit report.xml
|
|
37
|
+
|
|
38
|
+
# Watch the CI gate fail on purpose (1/3 passed, exit code 1):
|
|
39
|
+
.venv/bin/python -m prompt_eval run --cases examples/failing.yaml --provider stub; echo "exit=$?"
|
|
40
|
+
|
|
41
|
+
# Catch a prompt regression with run-diff (see
|
|
42
|
+
# examples/prompt-regression/README.md for the verified output):
|
|
43
|
+
.venv/bin/python -m prompt_eval run --cases examples/prompt-regression/cases.yaml \
|
|
44
|
+
--provider stub --prompt-version v1 --json /tmp/baseline.json
|
|
45
|
+
.venv/bin/python -m prompt_eval run --cases examples/prompt-regression/regressed.yaml \
|
|
46
|
+
--provider stub --prompt-version v2 --json /tmp/new.json
|
|
47
|
+
.venv/bin/python -m prompt_eval diff --baseline /tmp/baseline.json \
|
|
48
|
+
--new /tmp/new.json; echo "diff exit=$?"
|
|
49
|
+
# (The same baseline is also checked in as
|
|
50
|
+
# examples/prompt-regression/baseline.json.)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Verified in a clean venv against this checkout: golden cases
|
|
54
|
+
`30/30 passed`, failing example `1/3 passed` with exit code 1,
|
|
55
|
+
prompt-regression example `6/6` baseline vs `5/6` regressed with
|
|
56
|
+
exactly one `Pass -> Fail` case (`refund-window`), diff exit 1.
|
|
57
|
+
|
|
58
|
+
Use it as a library, too:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from prompt_eval import Case, Expectation, run_cases
|
|
62
|
+
from prompt_eval.providers import StubProvider
|
|
63
|
+
|
|
64
|
+
cases = [Case(id="greeting", prompt="Say hello",
|
|
65
|
+
expected=[Expectation(type="exact", params={"value": "Hello!"})])]
|
|
66
|
+
report = run_cases(cases, StubProvider({"greeting": "Hello!"}))
|
|
67
|
+
print(report.passed_count, "/", report.total, "gate:", report.gate_passed)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Case file format
|
|
71
|
+
|
|
72
|
+
A YAML file with a `cases` list. Each case has `prompt`, optional
|
|
73
|
+
`inputs` (substituted into `{placeholders}` in the prompt), an
|
|
74
|
+
`expected` list, and — for offline demos only — an optional
|
|
75
|
+
`stub_output`:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
version: 1
|
|
79
|
+
cases:
|
|
80
|
+
- id: json-person
|
|
81
|
+
prompt: "Return {name} as JSON"
|
|
82
|
+
inputs: {name: Ada}
|
|
83
|
+
expected:
|
|
84
|
+
- type: json_schema
|
|
85
|
+
schema:
|
|
86
|
+
type: object
|
|
87
|
+
required: [name, age]
|
|
88
|
+
properties:
|
|
89
|
+
name: {type: string}
|
|
90
|
+
age: {type: integer, minimum: 0}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Scorer catalogue
|
|
94
|
+
|
|
95
|
+
All scorers are deterministic and offline. None of them calls a model.
|
|
96
|
+
|
|
97
|
+
| Scorer | What it checks | Key parameters |
|
|
98
|
+
| ------ | -------------- | -------------- |
|
|
99
|
+
| `exact` | Output equals a value | `value`, `case_sensitive` (default true), `strip` (default true) |
|
|
100
|
+
| `regex` | Output matches a pattern | `pattern`, `ignore_case`, `multiline` |
|
|
101
|
+
| `json_schema` | Output parses as JSON and matches a full JSON Schema (via `jsonschema`) | `schema` |
|
|
102
|
+
| `length` | Character / word bounds | `min_length`, `max_length`, `min_words`, `max_words` |
|
|
103
|
+
| `format` | Output is well-formed | `kind`: `json`, `email`, `url`, `lowercase`, `uppercase` |
|
|
104
|
+
| `contains_all` | Output contains every listed string | `strings`, `case_sensitive` (default true) |
|
|
105
|
+
| `citation` | RAG-style output cites sources | `min_citations` (default 1), `pattern`, `required_sources` |
|
|
106
|
+
|
|
107
|
+
**JSON Schema (v0.2.0):** full validation via the `jsonschema`
|
|
108
|
+
package (Draft 2020-12), including `allOf`/`anyOf`/`oneOf`/`not`,
|
|
109
|
+
nested schemas, and format-independent keywords. An invalid schema
|
|
110
|
+
fails that expectation with a clear message instead of passing.
|
|
111
|
+
|
|
112
|
+
**Custom scorers:** register one in code with
|
|
113
|
+
`register_scorer(name, fn)`, or ship it via the
|
|
114
|
+
`prompt_eval.scorers` entry-point group. See
|
|
115
|
+
`examples/plugin_example.py` for a worked example.
|
|
116
|
+
|
|
117
|
+
**Citation default:** a citation is `[1]`, `[2]`, … or
|
|
118
|
+
`[source: name]`. Pass `pattern` to use a different marker style.
|
|
119
|
+
|
|
120
|
+
**LLM-as-judge (advisory only, v0.2.0):** add `--judge` to a run
|
|
121
|
+
to also ask a model to judge each output. Judge results are
|
|
122
|
+
reported in their own Markdown section, are never counted toward
|
|
123
|
+
pass rate or exit code, and a judge failure never fails the gate.
|
|
124
|
+
A non-deterministic judge cannot be a reproducible gate, so it
|
|
125
|
+
must stay advisory.
|
|
126
|
+
|
|
127
|
+
## Providers
|
|
128
|
+
|
|
129
|
+
- `stub` — returns each case's `stub_output`, or a placeholder
|
|
130
|
+
response. Offline, free, and the right choice for demos, tests,
|
|
131
|
+
and CI smoke runs. It proves the harness works; it does **not**
|
|
132
|
+
prove a model is good.
|
|
133
|
+
- `openai` — any OpenAI-compatible `/chat/completions` endpoint.
|
|
134
|
+
Configure with flags, never with a key in a file:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
export OPENAI_API_KEY="..." # from your provider, never commit it
|
|
138
|
+
python -m prompt_eval run --cases my-cases.yaml --provider openai \
|
|
139
|
+
--model gpt-4o-mini --prompt-version v3
|
|
140
|
+
# A different endpoint / key variable:
|
|
141
|
+
python -m prompt_eval run --cases my-cases.yaml --provider openai \
|
|
142
|
+
--base-url https://my-host.example/v1 --api-key-env MY_API_KEY_ENV
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Keys are read from the environment at run time and are never logged.
|
|
146
|
+
If the key variable is unset, the run fails with a clear error
|
|
147
|
+
instead of silently passing or failing cases. Transient provider
|
|
148
|
+
errors (HTTP 429/5xx, network errors) are retried with backoff
|
|
149
|
+
(`--retries`, default 2); other HTTP errors fail immediately.
|
|
150
|
+
|
|
151
|
+
- `ollama` — a local OpenAI-compatible server, default
|
|
152
|
+
`http://localhost:11434/v1`, no API key required:
|
|
153
|
+
`python -m prompt_eval run --cases my-cases.yaml --provider ollama --model llama3.1`
|
|
154
|
+
|
|
155
|
+
## Diff, record/replay, and Promptfoo import
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Compare two machine-readable runs (exit 1 if any case flipped pass -> fail):
|
|
159
|
+
python -m prompt_eval run --cases examples/cases.yaml --provider stub --json run-baseline.json
|
|
160
|
+
python -m prompt_eval run --cases examples/cases.yaml --provider stub --json run-new.json
|
|
161
|
+
python -m prompt_eval diff --baseline run-baseline.json --new run-new.json
|
|
162
|
+
|
|
163
|
+
# Record real outputs once, re-score them offline in CI (no model call):
|
|
164
|
+
python -m prompt_eval record --cases examples/cases.yaml --provider openai --recording rec.json
|
|
165
|
+
python -m prompt_eval replay --cases examples/cases.yaml --recording rec.json
|
|
166
|
+
|
|
167
|
+
# Import a basic Promptfoo file (deterministic assertions only; model
|
|
168
|
+
# assertions raise an error instead of being silently dropped):
|
|
169
|
+
python -m prompt_eval import-promptfoo --input promptfoo.yaml --output cases.yaml
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Token usage reported by the provider (prompt/completion/total
|
|
173
|
+
tokens) is shown in the Markdown report; cost is never estimated
|
|
174
|
+
from invented prices.
|
|
175
|
+
|
|
176
|
+
Worked example: `examples/prompt-regression/` is a realistic
|
|
177
|
+
6-case help-desk regression (baseline 6/6, one simulated
|
|
178
|
+
regression, diff names exactly that case) with its verified
|
|
179
|
+
output documented in its README.
|
|
180
|
+
|
|
181
|
+
## Reports and the CI gate
|
|
182
|
+
|
|
183
|
+
- `--markdown report.md` — summary table, gate result, run metadata
|
|
184
|
+
(provider/model, prompt version, harness version), and a section
|
|
185
|
+
per failed case.
|
|
186
|
+
- `--junit report.xml` — JUnit XML, so CI systems and GitHub code
|
|
187
|
+
scanning tooling can display failures per case.
|
|
188
|
+
- Exit code `0` means the gate passed (pass rate >= `--threshold`,
|
|
189
|
+
default `1.0`, i.e. every case must pass). Exit code `1` means a
|
|
190
|
+
case failed, the threshold was breached, or the provider failed.
|
|
191
|
+
Exit code `2` means a usage/config error (bad file, duplicate
|
|
192
|
+
case id, threshold out of range).
|
|
193
|
+
- `--threshold 0.9` allows a 90% pass rate; use it consciously, and
|
|
194
|
+
prefer `1.0` for a regression gate — a threshold below 1.0 lets
|
|
195
|
+
regressions through by design.
|
|
196
|
+
|
|
197
|
+
## CI guide
|
|
198
|
+
|
|
199
|
+
This repo dogfoods itself: `.github/workflows/ci.yml` runs the
|
|
200
|
+
pytest suite and the harness on its own golden cases on every push
|
|
201
|
+
and pull request (Python 3.11 and 3.12). For your own repo, copy
|
|
202
|
+
the template below, or use the bundled `action.yml` (see below).
|
|
203
|
+
The essential shape is:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
name: prompt-eval
|
|
207
|
+
on: [push, pull_request]
|
|
208
|
+
jobs:
|
|
209
|
+
eval:
|
|
210
|
+
runs-on: ubuntu-latest
|
|
211
|
+
steps:
|
|
212
|
+
- uses: actions/checkout@v4
|
|
213
|
+
- uses: actions/setup-python@v5
|
|
214
|
+
with: {python-version: "3.12"}
|
|
215
|
+
- run: pip install .
|
|
216
|
+
- run: >-
|
|
217
|
+
python -m prompt_eval run
|
|
218
|
+
--cases examples/cases.yaml --provider stub
|
|
219
|
+
--markdown report.md --junit report.xml
|
|
220
|
+
- uses: actions/upload-artifact@v4
|
|
221
|
+
if: always()
|
|
222
|
+
with: {name: prompt-eval-reports, path: "report.*"}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The bundled `action.yml` is a composite action wrapping the same
|
|
226
|
+
steps, so a consumer repo can call it with `cases`, `provider`,
|
|
227
|
+
`threshold`, and report-path inputs (see `action.yml`).
|
|
228
|
+
|
|
229
|
+
## Publishing (prepared, not yet published)
|
|
230
|
+
|
|
231
|
+
`.github/workflows/publish.yml` publishes to PyPI on version tags
|
|
232
|
+
via Trusted Publishing (OIDC, no token stored in the repo), and
|
|
233
|
+
`action.yml` already has Marketplace branding. The remaining steps
|
|
234
|
+
are account-owner steps only and are **not** done by this code:
|
|
235
|
+
create a PyPI account and enable 2FA, register a pending Trusted
|
|
236
|
+
Publisher for this repo/workflow on PyPI, and accept the GitHub
|
|
237
|
+
Marketplace Developer Agreement before publishing the Action.
|
|
238
|
+
|
|
239
|
+
## What v0.2.0 deliberately leaves out
|
|
240
|
+
|
|
241
|
+
- Hosted dashboard, SaaS, accounts, or any server component.
|
|
242
|
+
- Adapters for every LLM framework (LangChain, LangGraph,
|
|
243
|
+
LlamaIndex, …), every language, or a visual prompt editor.
|
|
244
|
+
- Training, fine-tuning, dataset generation, or a prompt
|
|
245
|
+
marketplace.
|
|
246
|
+
- A TypeScript port (a separate package later).
|
|
247
|
+
|
|
248
|
+
See [CHANGELOG.md](CHANGELOG.md) for the v0.2.0 scope,
|
|
249
|
+
[docs/user-guide.md](docs/user-guide.md) for the full guide, and
|
|
250
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for how to add a scorer.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prompt-eval-harness"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Deterministic, offline-first prompt and agent evaluation regression harness (library + CLI + GitHub Action)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "prompt-eval-harness contributors" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["PyYAML>=6.0", "jsonschema>=4.0"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/AsherLucas/prompt-eval-harness"
|
|
27
|
+
Repository = "https://github.com/AsherLucas/prompt-eval-harness"
|
|
28
|
+
Issues = "https://github.com/AsherLucas/prompt-eval-harness/issues"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=8.0"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
prompt-eval = "prompt_eval.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""prompt-eval-harness: deterministic, offline-first eval regression harness."""
|
|
2
|
+
|
|
3
|
+
from .judge import AdvisoryJudge
|
|
4
|
+
from .models import Case, Expectation, ExpectationResult, CaseResult, RunReport
|
|
5
|
+
from .promptfoo import import_promptfoo
|
|
6
|
+
from .runio import diff_runs, load_recording, load_run, save_recording, save_run
|
|
7
|
+
from .runner import replay_cases, run_cases
|
|
8
|
+
from .scorers import load_plugin_scorers, register_scorer, score_expectation
|
|
9
|
+
|
|
10
|
+
__version__ = "0.2.0"
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Case",
|
|
13
|
+
"Expectation",
|
|
14
|
+
"ExpectationResult",
|
|
15
|
+
"CaseResult",
|
|
16
|
+
"RunReport",
|
|
17
|
+
"run_cases",
|
|
18
|
+
"replay_cases",
|
|
19
|
+
"score_expectation",
|
|
20
|
+
"register_scorer",
|
|
21
|
+
"load_plugin_scorers",
|
|
22
|
+
"AdvisoryJudge",
|
|
23
|
+
"import_promptfoo",
|
|
24
|
+
"save_run",
|
|
25
|
+
"load_run",
|
|
26
|
+
"save_recording",
|
|
27
|
+
"load_recording",
|
|
28
|
+
"diff_runs",
|
|
29
|
+
"__version__",
|
|
30
|
+
]
|