jevdecision 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.
- jevdecision-0.1.0/CHANGELOG.md +25 -0
- jevdecision-0.1.0/LICENSE +21 -0
- jevdecision-0.1.0/MANIFEST.in +2 -0
- jevdecision-0.1.0/PKG-INFO +364 -0
- jevdecision-0.1.0/README.md +338 -0
- jevdecision-0.1.0/examples/support-ticket-cases.jsonl +12 -0
- jevdecision-0.1.0/examples/support-ticket-requirements.json +8 -0
- jevdecision-0.1.0/examples/support-ticket-responses.jsonl +12 -0
- jevdecision-0.1.0/examples/support-ticket.json +20 -0
- jevdecision-0.1.0/jevdecision/__init__.py +51 -0
- jevdecision-0.1.0/jevdecision/__main__.py +87 -0
- jevdecision-0.1.0/jevdecision/answers.py +307 -0
- jevdecision-0.1.0/jevdecision/backends.py +275 -0
- jevdecision-0.1.0/jevdecision/core.py +71 -0
- jevdecision-0.1.0/jevdecision/errors.py +39 -0
- jevdecision-0.1.0/jevdecision/evaluation.py +598 -0
- jevdecision-0.1.0/jevdecision/py.typed +0 -0
- jevdecision-0.1.0/jevdecision/questions.py +164 -0
- jevdecision-0.1.0/jevdecision.egg-info/PKG-INFO +364 -0
- jevdecision-0.1.0/jevdecision.egg-info/SOURCES.txt +26 -0
- jevdecision-0.1.0/jevdecision.egg-info/dependency_links.txt +1 -0
- jevdecision-0.1.0/jevdecision.egg-info/entry_points.txt +3 -0
- jevdecision-0.1.0/jevdecision.egg-info/top_level.txt +1 -0
- jevdecision-0.1.0/pyproject.toml +50 -0
- jevdecision-0.1.0/setup.cfg +4 -0
- jevdecision-0.1.0/tests/test_evaluation.py +602 -0
- jevdecision-0.1.0/tests/test_jevdecision.py +580 -0
- jevdecision-0.1.0/tests/test_release.py +250 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here, following [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-10-13
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Installable standard-library-only package, annotated public API, MIT license, source/sdist examples, and `jev-decision` / `jev-decision-eval` commands.
|
|
10
|
+
- Typed per-question refusals, refusal counts/rates, and score Brier, log-loss and ECE metrics.
|
|
11
|
+
- Versioned decision/evaluation/provenance records with producer metadata and canonical JSON.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Live evaluation durably appends responses and case results in a sibling staging directory; completed output is published atomically. Authentication failures stop with exit 2.
|
|
15
|
+
- Decision CLI returns exit 1 for a result containing refusals while retaining sibling answers.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Correctly rounded floating-point accumulation gives identical bundled replay metrics on Python 3.11 and 3.13.
|
|
19
|
+
- Examples ship once in the source/sdist, rather than being duplicated in the installed package.
|
|
20
|
+
- Missing score probabilities stay unknown; only complete distributions can derive a score. Missing metric inputs fail requirements.
|
|
21
|
+
- Probability values must be finite and within [0,1]; complete distributions must sum to 1 within rounding tolerance.
|
|
22
|
+
- HTTP protocol failures become retryable `APIError`; HTTP 408 is retryable.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
- Remote provider URLs require HTTPS; HTTP is limited to loopback hosts. URL credentials are rejected and authenticated POST redirects are never followed.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bicheng Wang
|
|
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,364 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jevdecision
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed AI-assisted decisions and replayable offline evaluation records
|
|
5
|
+
Author: Bicheng Wang
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BichengWang/ai-decision-system/tree/main/ai-workflows/jev-decision
|
|
8
|
+
Project-URL: Source, https://github.com/BichengWang/ai-decision-system
|
|
9
|
+
Project-URL: Issues, https://github.com/BichengWang/ai-decision-system/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/BichengWang/ai-decision-system/blob/main/ai-workflows/jev-decision/CHANGELOG.md
|
|
11
|
+
Keywords: decisions,evaluation,replay,probability,gating
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Jev decision model (`jevdecision`)
|
|
28
|
+
|
|
29
|
+
Replayable decision records for AI-assisted go/no-go decisions. A decision model answers bounded
|
|
30
|
+
questions about a state with **typed answers and reported probabilities** instead of
|
|
31
|
+
generated text: a yes/no probability, one label from a fixed set, or a position
|
|
32
|
+
on an ordered scale. These can inform gating agent tool calls, routing tickets,
|
|
33
|
+
and triage; missing choice uncertainty never becomes permission to route.
|
|
34
|
+
|
|
35
|
+
[Jev](https://docs.typesafe.ai/api) (TypeSafe's System One model) is the default
|
|
36
|
+
backend. The [OpenAI Decisions API](https://developers.openai.com/api/docs/guides/decisions)
|
|
37
|
+
is a drop-in alternative: questions are defined once and answers come back in the
|
|
38
|
+
same types from either provider. The package uses only the Python standard
|
|
39
|
+
library, so it has no dependencies and no lockfile.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11 or later; runtime dependencies: none. Examples live in the clone
|
|
44
|
+
and source distribution under `examples/`; they are not installed in the wheel.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Once 0.1.0 is published:
|
|
48
|
+
pip install jevdecision
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The distribution includes `jev-decision` and `jev-decision-eval`; existing
|
|
52
|
+
`python -m jevdecision` and `python -m jevdecision.evaluation` commands remain supported.
|
|
53
|
+
Use the `examples/` directory in a clone or unpacked source distribution.
|
|
54
|
+
|
|
55
|
+
## Question types
|
|
56
|
+
|
|
57
|
+
| Type | Jev request | OpenAI request | Answer |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `Noul` (alias `Predicate`) | `noul` | `predicate` | `NoulAnswer.probability`: probability of yes |
|
|
60
|
+
| `Choice` | `choice`, `criteria` = label → description | `choice`, `choices` = `[{value, description}]` | `ChoiceAnswer.choice`, `.probabilities` per label, `.confidence` |
|
|
61
|
+
| `Score` | `score`, `criteria` = ordered levels | `score`, `levels` = `[{label}]` | `ScoreAnswer.score` (probability-weighted level index), `.level`, `.probabilities` per level |
|
|
62
|
+
|
|
63
|
+
Questions are validated before anything is sent: names are unique identifiers,
|
|
64
|
+
instructions are non-empty, a choice has 1 to 255 options, and a score has 2 to
|
|
65
|
+
10 distinct levels. Responses are checked against the questions they answer.
|
|
66
|
+
Every question must have a matching answer or an explicit `type: "refusal"` result; a choice must be one of the
|
|
67
|
+
options, and probabilities and scores must lie in range. Probabilities must be finite and within [0,1]. Complete distributions must sum
|
|
68
|
+
to 1 within `1e-6 * number_of_options + 1e-3`; partial collections retain unknown
|
|
69
|
+
entries and are not treated as complete distributions. Invalid responses raise
|
|
70
|
+
`ResponseError`, so the application never acts on a malformed answer.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from jevdecision import Choice, DecisionModel, Noul, Score
|
|
76
|
+
|
|
77
|
+
model = DecisionModel.from_env() # DECISION_BACKEND=jev (default) or openai
|
|
78
|
+
|
|
79
|
+
decision = model.decide(
|
|
80
|
+
"Help! My payouts have been failing for 3 days.",
|
|
81
|
+
[
|
|
82
|
+
Noul("refund", "Is the customer asking for a refund?"),
|
|
83
|
+
Choice("department", "Which team should handle this?", {
|
|
84
|
+
"billing": "Payments, invoicing, refunds",
|
|
85
|
+
"technical": "Bugs, outages, integrations",
|
|
86
|
+
"sales": "Pricing, upgrades, new accounts",
|
|
87
|
+
}),
|
|
88
|
+
Score("frustration", "How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"]),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
decision["department"].choice # "technical"
|
|
92
|
+
decision["frustration"].level # "Frustrated"
|
|
93
|
+
|
|
94
|
+
# Single-question helpers
|
|
95
|
+
model.gate(state, refund_question, threshold=0.8) # bool
|
|
96
|
+
model.route(state, department_question, min_confidence=0.7,
|
|
97
|
+
fallback="human_review") # label or fallback
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`route` uses the provider's `confidence` when it returns one, otherwise the
|
|
101
|
+
reported probability of the chosen label. If neither is supplied, it returns
|
|
102
|
+
`fallback` (default `None`), even with `min_confidence=0.0`. A reported confidence
|
|
103
|
+
still takes precedence over a reported choice probability; neither is a calibrated
|
|
104
|
+
safety guarantee or a replacement for application policy.
|
|
105
|
+
|
|
106
|
+
### Refusals and score uncertainty
|
|
107
|
+
|
|
108
|
+
An explicit per-question refusal becomes `RefusalAnswer` with `.name` and
|
|
109
|
+
`.type == "refusal"`. `ask`/`decide` retain valid sibling answers. `gate` returns
|
|
110
|
+
`False` for a refusal, even at threshold zero; `route` returns its fallback.
|
|
111
|
+
The OpenAI guide documents this marker; TypeSafe's reference does not document
|
|
112
|
+
refusal fields, so no reason or other provider-specific field is assumed.
|
|
113
|
+
|
|
114
|
+
Score probabilities are a tuple in level order, with `None` for missing entries.
|
|
115
|
+
`.probability` is the reported probability of the nearest level, or `None`.
|
|
116
|
+
A supplied score remains usable without probabilities; deriving a missing score
|
|
117
|
+
requires a complete distribution. Missing values are never zero-filled.
|
|
118
|
+
|
|
119
|
+
### Choice uncertainty contract
|
|
120
|
+
|
|
121
|
+
`ask` and `decide` retain a valid choice even when its uncertainty is unknown:
|
|
122
|
+
|
|
123
|
+
- `.probabilities` contains only labels whose probabilities were actually supplied,
|
|
124
|
+
in question-option order. Missing, null, or empty probability collections yield `{}`.
|
|
125
|
+
- `.probability` is `None` when the chosen label has no reported probability.
|
|
126
|
+
A supplied `confidence` does not manufacture a choice probability.
|
|
127
|
+
- `.ranked()` ranks only reported probabilities. `to_dict()` preserves missing
|
|
128
|
+
entries instead of fabricating a complete distribution.
|
|
129
|
+
- A reported `0.0` remains zero, distinct from an absent value.
|
|
130
|
+
|
|
131
|
+
**Safety correction:** earlier code fabricated probability `1.0` for a choice
|
|
132
|
+
without probabilities and filled omitted labels with `0.0`. Callers must use
|
|
133
|
+
`.probability` or `.probabilities.get(label)` and handle `None`; do not rely on
|
|
134
|
+
every label being present. To inspect an unscored recommendation, use `ask` or
|
|
135
|
+
`decide`, not a confidence-gated route with an invented probability.
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
| Variable | Backend | Meaning |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `DECISION_BACKEND` | both | `jev` (default) or `openai` |
|
|
142
|
+
| `TYPESAFE_API_KEY` | `jev` | Bearer token for `https://api.typesafe.ai/v1/systemone` |
|
|
143
|
+
| `JEV_MODEL` | `jev` | Model route (default `jev-latest`) |
|
|
144
|
+
| `OPENAI_API_KEY` | `openai` | Bearer token for `https://api.openai.com/v1/decisions` |
|
|
145
|
+
| `OPENAI_DECISIONS_MODEL` | `openai` | Model (default `gpt-6-luna`) |
|
|
146
|
+
|
|
147
|
+
Every backend also takes `api_key`, `model`, `base_url`, `path`, `timeout`,
|
|
148
|
+
`max_retries`, `backoff`, `max_retry_wait`, and `transport` arguments.
|
|
149
|
+
`max_retries` must be a non-negative integer; zero disables retries. `backoff`
|
|
150
|
+
must be a finite non-negative number of seconds; zero permits immediate retries
|
|
151
|
+
unless `Retry-After` requests a wait. Invalid settings raise `DecisionError`
|
|
152
|
+
before any request is sent. Use `base_url` and `path` for a gateway
|
|
153
|
+
that serves Jev elsewhere, for example `path="/v1/decisions"` with
|
|
154
|
+
`model="typesafe/jev"`. Request timeout (408), rate-limit (429), server (5xx), connection and HTTP protocol errors are
|
|
155
|
+
retried with exponential backoff (2 retries by default). When the provider sends a
|
|
156
|
+
`Retry-After` header (seconds or an HTTP date), the client waits at least that long
|
|
157
|
+
before retrying. A provider that asks for more than `max_retry_wait` seconds (60 by
|
|
158
|
+
default) fails the request at once with `APIError`, whose `retry_after` attribute
|
|
159
|
+
carries the requested wait, instead of blocking the caller. Other HTTP errors are
|
|
160
|
+
raised as `APIError` immediately. `transport` is any callable
|
|
161
|
+
`(url, headers, body_bytes, timeout) -> dict`, which tests use to replace the
|
|
162
|
+
network.
|
|
163
|
+
|
|
164
|
+
Remote `base_url` values require HTTPS. HTTP is accepted only for `localhost`
|
|
165
|
+
or a literal loopback IP address (IPv4 or IPv6). Credentials, query strings and
|
|
166
|
+
fragments in the base URL are rejected with `QuestionError`. Authenticated POSTs
|
|
167
|
+
never follow redirects: every 3xx response raises a non-retryable `APIError`.
|
|
168
|
+
Custom transports must enforce equivalent credential protection themselves.
|
|
169
|
+
|
|
170
|
+
The structured state can be text, a JSON object, or a list of non-empty strings. Jev
|
|
171
|
+
receives it unchanged. The OpenAI backend sends text, so it serializes an object
|
|
172
|
+
as JSON with sorted keys and joins a list with blank lines.
|
|
173
|
+
|
|
174
|
+
## Command line
|
|
175
|
+
|
|
176
|
+
From this directory, with Python 3.11 or later:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python3 -m unittest discover -s tests
|
|
180
|
+
python3 -m jevdecision --questions examples/support-ticket.json \
|
|
181
|
+
--state "Help! My payouts have been failing for 3 days." --dry-run
|
|
182
|
+
python3 -m jevdecision --questions examples/support-ticket.json \
|
|
183
|
+
--state-file ticket.json --backend openai
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The questions file uses Jev's native question map
|
|
187
|
+
([`examples/support-ticket.json`](examples/support-ticket.json)). `predicate` is
|
|
188
|
+
accepted as an alias of `noul`. A `.json` state file is sent as structured
|
|
189
|
+
state, and any other file is sent as text. `--dry-run` prints the provider request
|
|
190
|
+
without sending it, so it needs no API key; the state is validated first either
|
|
191
|
+
way. The decision is printed as JSON. Exit codes: 0 for success, 1 for a result containing refusals or a provider
|
|
192
|
+
or response error, 2 for invalid input (questions, state, or a `--timeout` that is
|
|
193
|
+
not a positive number of seconds).
|
|
194
|
+
|
|
195
|
+
From the repository root, `make jev-test` runs the tests and
|
|
196
|
+
`make jev-dry-run` prints the example request for both backends.
|
|
197
|
+
|
|
198
|
+
## Evaluating a decision model
|
|
199
|
+
|
|
200
|
+
Measure the model on labeled cases before software acts on its answers.
|
|
201
|
+
`jevdecision.evaluation` scores every question against the expected answers and
|
|
202
|
+
can gate the result on thresholds you set.
|
|
203
|
+
|
|
204
|
+
A case file is JSON Lines with a state and the expected answer to some or all
|
|
205
|
+
questions: `true` or `false` for a `noul`, a label for a `choice`, and a level
|
|
206
|
+
(or its index) for a `score`:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{"id": "t02", "state": "I was charged twice for March. Please refund the duplicate charge.",
|
|
210
|
+
"labels": {"refund": true, "department": "billing", "frustration": "Frustrated"}}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| Type | Metrics |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `noul` | `accuracy` (at probability 0.5), `brier`, `log_loss`, `ece`, `base_rate`, `mean_probability` |
|
|
216
|
+
| `choice` | `accuracy`, `brier` (summed over labels), `log_loss`, `ece` (probability of the chosen label against correctness), a confusion matrix |
|
|
217
|
+
| `score` | `accuracy` (nearest level), `within_one`, `mae` (in level steps), `brier`, `log_loss`, `ece` (nearest level probability) |
|
|
218
|
+
|
|
219
|
+
Every question also reports `n`, the number of cases that label it, and
|
|
220
|
+
`accuracy_lower` and `accuracy_upper`, the 95% Wilson score interval for
|
|
221
|
+
`accuracy`. With 12 cases, 11 correct answers give an accuracy of 0.917 but an
|
|
222
|
+
interval of [0.646, 0.985], so a gate on `accuracy_lower` asks for enough cases as
|
|
223
|
+
well as a high hit rate. `ece` is the
|
|
224
|
+
expected calibration error over ten equal-width probability bins. Log loss clips
|
|
225
|
+
probabilities to [1e-6, 1 - 1e-6].
|
|
226
|
+
|
|
227
|
+
Every question also reports `refusals` and `refusal_rate` among its labeled,
|
|
228
|
+
parsed results. Refusals count in `n`, accuracy and its Wilson interval as
|
|
229
|
+
incorrect, and make probability metrics unmeasured. Score MAE and within-one
|
|
230
|
+
are also unmeasured if any result refuses. Request/parse errors remain separate.
|
|
231
|
+
|
|
232
|
+
Missing choice probabilities do not exclude otherwise valid answers from `n`,
|
|
233
|
+
accuracy, its interval, or the confusion matrix. Probability metrics require the
|
|
234
|
+
inputs they measure on **every scored case**, not a selectively observed subset:
|
|
235
|
+
|
|
236
|
+
- Brier score requires a reported probability for every option.
|
|
237
|
+
- Log loss requires the expected label's reported probability.
|
|
238
|
+
- ECE requires the chosen label's reported probability; provider `confidence`
|
|
239
|
+
is not substituted for that probability.
|
|
240
|
+
|
|
241
|
+
If any scored case lacks a metric's required inputs, that metric is `null` in
|
|
242
|
+
JSON and `unmeasured` in the report. A requirement on it fails. Per-case records
|
|
243
|
+
retain the prediction and a null chosen probability, and raw responses remain
|
|
244
|
+
available for replay. Re-evaluating older responses with missing probabilities
|
|
245
|
+
may therefore fail gates that previously scored fabricated values. Complete
|
|
246
|
+
probability responses retain their previous scores. Score Brier requires every
|
|
247
|
+
level probability, log loss requires the expected level probability, and ECE
|
|
248
|
+
requires the nearest level probability on every case. Missing inputs yield
|
|
249
|
+
`null` and fail metric requirements, as for choices.
|
|
250
|
+
|
|
251
|
+
A requirement file supplies explicit question metric bounds, each a `min` or
|
|
252
|
+
`max`. The overall JSON field is `status` (not a boolean `pass`):
|
|
253
|
+
|
|
254
|
+
- `PASS`: at least one valid question metric bound was supplied, and all checks pass.
|
|
255
|
+
- `FAIL`: an explicit metric bound or the error-rate check fails.
|
|
256
|
+
- `UNGATED`: all checks pass but there are no question metric bounds. Omitting
|
|
257
|
+
`--require`, `{}`, `{"max_error_rate": 0}`, and `{"questions": {}}` are all
|
|
258
|
+
ungated, not model-quality validation.
|
|
259
|
+
|
|
260
|
+
`max_error_rate` (default 0) bounds the share of cases whose request or response
|
|
261
|
+
failed; it alone is not a question gate. A failed case counts as an error, never
|
|
262
|
+
as a wrong answer. A bound on an unmeasured metric fails. A valid question bound
|
|
263
|
+
means a finite numeric `min`/`max` on a supported metric; it does not mean the
|
|
264
|
+
threshold is scientifically adequate. For example, an `n`-only gate checks sample
|
|
265
|
+
count, not accuracy. Select meaningful thresholds for the decision being made.
|
|
266
|
+
Malformed specs (including JSON `null`, `false`, arrays, empty per-question metric
|
|
267
|
+
objects and empty bounds) are invalid input, not ungated runs. In the Python
|
|
268
|
+
`evaluate()` API, `None` means no requirement spec. Individual requirement rows
|
|
269
|
+
retain boolean `pass` values; the overall report does not.
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{"max_error_rate": 0.0,
|
|
273
|
+
"questions": {"refund": {"n": {"min": 10}, "accuracy_lower": {"min": 0.8}, "brier": {"max": 0.1}}}}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Replay supplied responses: no API key; origin remains unverified
|
|
278
|
+
python3 -m jevdecision.evaluation --questions examples/support-ticket.json \
|
|
279
|
+
--cases examples/support-ticket-cases.jsonl \
|
|
280
|
+
--responses examples/support-ticket-responses.jsonl \
|
|
281
|
+
--require examples/support-ticket-requirements.json --out eval-run
|
|
282
|
+
|
|
283
|
+
# Ask the provider live (needs its API key); no question bounds means UNGATED
|
|
284
|
+
python3 -m jevdecision.evaluation --questions examples/support-ticket.json \
|
|
285
|
+
--cases examples/support-ticket-cases.jsonl --backend openai --out live-run
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`--out` must be new or empty. Files are prepared in a sibling `.NAME-*` staging
|
|
289
|
+
directory, then published with `os.replace` after all summaries are complete.
|
|
290
|
+
Live runs print the staging path and append/fsync each completed response and
|
|
291
|
+
case result before requesting the next case, including malformed JSON responses
|
|
292
|
+
that parsed as JSON but failed answer validation. Interrupted runs retain these
|
|
293
|
+
JSONL files for inspection and replay; the target remains available for a rerun.
|
|
294
|
+
A 401/403 stops immediately with exit 2 and retains progress without a final report.
|
|
295
|
+
There is no automatic resume; replay the saved responses with the original cases
|
|
296
|
+
(missing responses count as errors), or start a new live run.
|
|
297
|
+
|
|
298
|
+
Each run writes `evaluation.json` (canonical JSON),
|
|
299
|
+
a readable `EVALUATION.md`, `responses.jsonl` with the supplied replay responses
|
|
300
|
+
or responses obtained through the live backend, and `cases.jsonl` with one row
|
|
301
|
+
per case: each labeled question's expected and predicted answer (with probability
|
|
302
|
+
or score) and whether it was correct, or the case's error. `EVALUATION.md` lists
|
|
303
|
+
the first 20 incorrect answers. Exported responses can be passed as `--responses`
|
|
304
|
+
to re-score without calling a provider. The bundled replay produces byte-identical
|
|
305
|
+
`evaluation.json`, `provenance.json`,
|
|
306
|
+
`responses.jsonl` and `cases.jsonl` on Python 3.11 and 3.13; live-to-replay
|
|
307
|
+
metrics remain the same, but the report's `source` changes.
|
|
308
|
+
|
|
309
|
+
Decision records use `schema_version: 1`; evaluation and provenance records use
|
|
310
|
+
`schema_version: 2`. They carry `produced_by: {"name": "jevdecision", "version": "0.1.0"}`.
|
|
311
|
+
Raw provider response replay remains compatible with existing unversioned fixtures.
|
|
312
|
+
JSON records use sorted keys, compact separators and UTF-8 without ASCII escaping;
|
|
313
|
+
SHA-256 digests are lowercase hexadecimal. JSONL rows use the same encoding.
|
|
314
|
+
|
|
315
|
+
CLI runs also write a separate deterministic `provenance.json`:
|
|
316
|
+
|
|
317
|
+
- `inputs` records SHA-256 of the exact question, case, requirement and response
|
|
318
|
+
file bytes read for the run, with `null` for absent optional inputs.
|
|
319
|
+
- `response_mode` is `live` or `replay`. Replay origin is explicitly **unverified**:
|
|
320
|
+
files may be hand-written, and their `model` fields do not authenticate provider output.
|
|
321
|
+
- `outputs` records SHA-256 of `evaluation.json`, `EVALUATION.md`, `responses.jsonl`
|
|
322
|
+
and `cases.jsonl`. The manifest does not hash itself, include timestamps, paths,
|
|
323
|
+
headers or credentials, or enter the metric records. Whitespace-only input
|
|
324
|
+
changes can therefore change provenance without changing `evaluation.json`.
|
|
325
|
+
|
|
326
|
+
Hashes identify bytes, not authenticity, reviewer independence or model quality.
|
|
327
|
+
Raw responses and labeled cases may contain sensitive material; inspect them
|
|
328
|
+
before sharing. `--backend` names the response format to replay (`jev` by default).
|
|
329
|
+
The CLI and Markdown display the same overall status. Exit codes: **0** for PASS,
|
|
330
|
+
**1** for FAIL or a provider that cannot be used, **2** for invalid input/unreadable files or live 401/403 (authentication failures
|
|
331
|
+
retain staged progress; invalid input produces no final artifacts), and **3** for UNGATED.
|
|
332
|
+
|
|
333
|
+
The example responses are hand-written in Jev's response format to show the
|
|
334
|
+
workflow. They are not output from Jev or any other model, and the example
|
|
335
|
+
report says nothing about any model's quality.
|
|
336
|
+
|
|
337
|
+
From the repository root, `make jev-eval` runs the example into a temporary directory.
|
|
338
|
+
|
|
339
|
+
## Limitations
|
|
340
|
+
|
|
341
|
+
- Both provider APIs are new. The Jev request and response shapes follow
|
|
342
|
+
TypeSafe's published examples. The OpenAI Decisions API is in public beta, so
|
|
343
|
+
the parser accepts both its object and its list answer layouts. Recheck the
|
|
344
|
+
field mapping against the provider references before you depend on it in
|
|
345
|
+
production.
|
|
346
|
+
- Image inputs and per-request options beyond the model are not modeled yet.
|
|
347
|
+
- Unit tests use recorded response shapes and never call a provider.
|
|
348
|
+
- Only accuracy carries a confidence interval. Brier, log loss, calibration
|
|
349
|
+
error and MAE are point estimates, which are noisy with a few dozen cases, so set
|
|
350
|
+
`n` minimums that match the thresholds you rely on.
|
|
351
|
+
- A live evaluation sends one request per case, sequentially.
|
|
352
|
+
- Float sums use `math.fsum` for correctly rounded accumulation. The bundled replay
|
|
353
|
+
has identical bytes and hashes on Python 3.11 and 3.13; this check is not a
|
|
354
|
+
guarantee for every platform, math library or future algorithm version.
|
|
355
|
+
|
|
356
|
+
## Local software verification
|
|
357
|
+
|
|
358
|
+
Release checks cover refusals, unknown probabilities, malformed distributions,
|
|
359
|
+
HTTP retries and redirect protection, interrupted live progress, authentication
|
|
360
|
+
stops and atomic publication. Tests use synthetic provider responses and a local
|
|
361
|
+
HTTP fixture. Fresh wheel installs exercise both backend dry runs and the gated
|
|
362
|
+
12-case replay example; exported responses reproduce the evaluation bytes across
|
|
363
|
+
Python 3.11 and 3.13. These checks are not live-provider calls, calibration results,
|
|
364
|
+
or deployment-safety evidence.
|