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.
Files changed (28) hide show
  1. prompt_eval_harness-0.2.0/LICENSE +21 -0
  2. prompt_eval_harness-0.2.0/PKG-INFO +275 -0
  3. prompt_eval_harness-0.2.0/README.md +250 -0
  4. prompt_eval_harness-0.2.0/pyproject.toml +40 -0
  5. prompt_eval_harness-0.2.0/setup.cfg +4 -0
  6. prompt_eval_harness-0.2.0/src/prompt_eval/__init__.py +30 -0
  7. prompt_eval_harness-0.2.0/src/prompt_eval/__main__.py +6 -0
  8. prompt_eval_harness-0.2.0/src/prompt_eval/cli.py +205 -0
  9. prompt_eval_harness-0.2.0/src/prompt_eval/judge.py +49 -0
  10. prompt_eval_harness-0.2.0/src/prompt_eval/models.py +180 -0
  11. prompt_eval_harness-0.2.0/src/prompt_eval/promptfoo.py +103 -0
  12. prompt_eval_harness-0.2.0/src/prompt_eval/providers.py +132 -0
  13. prompt_eval_harness-0.2.0/src/prompt_eval/reports.py +86 -0
  14. prompt_eval_harness-0.2.0/src/prompt_eval/runio.py +100 -0
  15. prompt_eval_harness-0.2.0/src/prompt_eval/runner.py +95 -0
  16. prompt_eval_harness-0.2.0/src/prompt_eval/scorers.py +196 -0
  17. prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/PKG-INFO +275 -0
  18. prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/SOURCES.txt +26 -0
  19. prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/dependency_links.txt +1 -0
  20. prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/entry_points.txt +2 -0
  21. prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/requires.txt +5 -0
  22. prompt_eval_harness-0.2.0/src/prompt_eval_harness.egg-info/top_level.txt +1 -0
  23. prompt_eval_harness-0.2.0/tests/test_cli.py +60 -0
  24. prompt_eval_harness-0.2.0/tests/test_new_features.py +166 -0
  25. prompt_eval_harness-0.2.0/tests/test_prompt_regression_example.py +44 -0
  26. prompt_eval_harness-0.2.0/tests/test_providers.py +55 -0
  27. prompt_eval_harness-0.2.0/tests/test_runner_reports.py +47 -0
  28. 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
+ [![CI](https://github.com/AsherLucas/prompt-eval-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/AsherLucas/prompt-eval-harness/actions/workflows/ci.yml)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
30
+ [![Python >= 3.11](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg)](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
+ [![CI](https://github.com/AsherLucas/prompt-eval-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/AsherLucas/prompt-eval-harness/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+ [![Python >= 3.11](https://img.shields.io/badge/python-%3E%3D3.11-blue.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow `python -m prompt_eval run ...`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())