jev-trust 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.
- jev_trust-0.1.0/LICENSE +21 -0
- jev_trust-0.1.0/PKG-INFO +135 -0
- jev_trust-0.1.0/README.md +115 -0
- jev_trust-0.1.0/jev_trust/__init__.py +32 -0
- jev_trust-0.1.0/jev_trust/calib.py +148 -0
- jev_trust-0.1.0/jev_trust/client.py +106 -0
- jev_trust-0.1.0/jev_trust/ed25519.py +129 -0
- jev_trust-0.1.0/jev_trust/receipt.py +108 -0
- jev_trust-0.1.0/jev_trust/trust.py +216 -0
- jev_trust-0.1.0/jev_trust.egg-info/PKG-INFO +135 -0
- jev_trust-0.1.0/jev_trust.egg-info/SOURCES.txt +15 -0
- jev_trust-0.1.0/jev_trust.egg-info/dependency_links.txt +1 -0
- jev_trust-0.1.0/jev_trust.egg-info/top_level.txt +1 -0
- jev_trust-0.1.0/pyproject.toml +28 -0
- jev_trust-0.1.0/setup.cfg +4 -0
- jev_trust-0.1.0/tests/test_calib.py +79 -0
- jev_trust-0.1.0/tests/test_trust.py +183 -0
jev_trust-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nautilus Assay (Yiluo Technology)
|
|
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.
|
jev_trust-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jev-trust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trust middleware for Jev decision APIs - log every call, measure calibration in YOUR domain, spend confidence at its verified exchange rate.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/chunxiaoxx/nautilus-compass
|
|
7
|
+
Project-URL: Research, https://github.com/chunxiaoxx/nautilus-compass/discussions/59
|
|
8
|
+
Project-URL: Wall, https://compass.nautilus.social/wall.html
|
|
9
|
+
Keywords: jev,calibration,trust,ai,agents,decision-making,verification,ece,brier
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# jev-trust
|
|
22
|
+
|
|
23
|
+
**Trust middleware for Jev decision APIs — spend confidence at its verified exchange rate.**
|
|
24
|
+
|
|
25
|
+
Jev gives you calibrated-looking probabilities. But calibration is a property of
|
|
26
|
+
**(model, domain)** pairs, not models. We measured hosted Jev 1.13 at
|
|
27
|
+
ECE 0.041 on closed deterministic tasks — and **accuracy 50% while stating
|
|
28
|
+
91% confidence** on synthetic email triage ([Assay research #1/#2](https://github.com/chunxiaoxx/nautilus-compass/discussions/59)).
|
|
29
|
+
Same model. Same day. The number you actually care about is the one for *your* domain,
|
|
30
|
+
and nobody has measured it yet — including the vendor.
|
|
31
|
+
|
|
32
|
+
`jev-trust` wraps the Jev API and measures it in *your* domain while you work:
|
|
33
|
+
|
|
34
|
+
- **Every call logged** — append-only JSONL: question, answer, stated confidence, effective confidence, usage, timestamp.
|
|
35
|
+
- **Outcomes fed back** — as ground truth arrives, `record_outcome()` builds your domain's calibration record: accuracy, Brier, ECE, and calibration currency **C = 1 − ECE**.
|
|
36
|
+
- **Effective confidence on every answer** — `r.effective_confidence` = what the stated confidence is *worth* here so far (observed accuracy of the matching confidence bin, ≥5 samples; else C-adjusted; else `None` = insufficient evidence).
|
|
37
|
+
- **Overconfidence alerts** — a callback fires when a decision is confident enough to act on but the domain hasn't earned that confidence yet.
|
|
38
|
+
- **Signed evidence** — one call signs the whole log (ed25519). Publish log + sig + pubkey; anyone — including [Nautilus Assay](https://compass.nautilus.social/wall.html) — can independently recompute your numbers.
|
|
39
|
+
|
|
40
|
+
Zero dependencies. Pure stdlib. Python ≥ 3.8.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install jev-trust
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from jev_trust import TrustedJev
|
|
52
|
+
|
|
53
|
+
jev = TrustedJev(api_key=API_KEY, domain="email-triage")
|
|
54
|
+
|
|
55
|
+
# decide() never lets unexamined confidence through:
|
|
56
|
+
r = jev.decide(state, {"q": {"type": "choice", "instructions": "...",
|
|
57
|
+
"criteria": {...}}})["q"]
|
|
58
|
+
|
|
59
|
+
r.decision # 'alpha'
|
|
60
|
+
r.stated_confidence # 0.91 <- what Jev claims
|
|
61
|
+
r.effective_confidence # 0.50 <- what 0.91 is worth in YOUR domain so far
|
|
62
|
+
r.basis # 'bin_observed' | 'C_adjusted' | 'insufficient_n'
|
|
63
|
+
r.domain_verdict # FACE_VALUE | DISCOUNT | DOWNGRADE | UNVERIFIED
|
|
64
|
+
|
|
65
|
+
# feed ground truth back as it arrives:
|
|
66
|
+
jev.record_outcome("q", truth)
|
|
67
|
+
|
|
68
|
+
# your domain's running scorecard:
|
|
69
|
+
jev.stats()
|
|
70
|
+
# {'n_outcomes': 42, 'accuracy': 0.52, 'brier': 0.41, 'ece': 0.39,
|
|
71
|
+
# 'C': 0.61, 'verdict': 'DISCOUNT', ...}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## The alert that pays for itself
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
def on_overconfidence(r):
|
|
78
|
+
# fired when stated >= 0.90 but the domain hasn't earned FACE_VALUE
|
|
79
|
+
log.warning(f"{r.qid}: Jev says {r.stated_confidence}, "
|
|
80
|
+
f"worth {r.effective_confidence} here — route to human?")
|
|
81
|
+
|
|
82
|
+
jev = TrustedJev(api_key=API_KEY, domain="fraud-flag",
|
|
83
|
+
alert_confidence=0.90, on_overconfidence=on_overconfidence)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Verdicts (Assay calibration-currency reading levels)
|
|
87
|
+
|
|
88
|
+
| Verdict | Condition | Reading |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `UNVERIFIED` | < 20 outcomes in domain | stated confidence is an unbacked claim |
|
|
91
|
+
| `FACE_VALUE` | C ≥ 0.80 | use stated confidence as-is |
|
|
92
|
+
| `DISCOUNT` | 0.50 ≤ C < 0.80 | multiply trust by C |
|
|
93
|
+
| `DOWNGRADE` | C < 0.50 | route to human review |
|
|
94
|
+
|
|
95
|
+
The library ships with Assay's published reference rates (informational — your
|
|
96
|
+
verdict is always computed from *your* outcomes):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
jev_trust.ASSAY_REFERENCE_RATES
|
|
100
|
+
# {'closed-deterministic': {'C': 0.959, ...}, # n=240, ECE 0.041
|
|
101
|
+
# 'adversarial-stress': {'C': 0.988, ...}, # n=200, ECE 0.012
|
|
102
|
+
# 'synthetic-email-choice': {'C': 0.086, ...}} # acc 50% @ conf 91%
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Signed evidence — make your numbers independently checkable
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
sig_path = jev.sign_log() # writes <log>.jsonl.sig
|
|
109
|
+
jev.keys.pub_hex # publish this next to the log
|
|
110
|
+
|
|
111
|
+
# anyone can verify:
|
|
112
|
+
from jev_trust import verify_log
|
|
113
|
+
verify_log("session.jsonl", "session.jsonl.sig", pub_hex) # -> VALID
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Same canonical-JSON + ed25519 scheme as
|
|
117
|
+
[assay-verify](https://pypi.org/project/assay-verify/) — a signed `jev-trust`
|
|
118
|
+
log is directly submittable to Nautilus Assay for independent recomputation
|
|
119
|
+
(the evidence format behind their [Domain Calibration Reports](https://compass.nautilus.social/dcr.html)).
|
|
120
|
+
|
|
121
|
+
## Why this exists
|
|
122
|
+
|
|
123
|
+
Nautilus Assay independently verifies AI-agent performance claims. Their two
|
|
124
|
+
public Jev studies found: overall accuracy 92.2% / Brier 0.048 on closed tasks
|
|
125
|
+
(good), but domain calibration collapses on distribution shift — 50% accuracy
|
|
126
|
+
at 91% stated confidence in one synthetic triage domain. Vendor benchmarks
|
|
127
|
+
can't see your domain. `jev-trust` is the always-on instrument that can.
|
|
128
|
+
|
|
129
|
+
Methodology: ECE = equal-width 10-bin top-label; Brier = mean (1 − p_true)²
|
|
130
|
+
(binary-identical to the standard (p − y)² form). Identical formulas to Assay's
|
|
131
|
+
published verification code.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. © 2026 Nautilus Assay.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# jev-trust
|
|
2
|
+
|
|
3
|
+
**Trust middleware for Jev decision APIs — spend confidence at its verified exchange rate.**
|
|
4
|
+
|
|
5
|
+
Jev gives you calibrated-looking probabilities. But calibration is a property of
|
|
6
|
+
**(model, domain)** pairs, not models. We measured hosted Jev 1.13 at
|
|
7
|
+
ECE 0.041 on closed deterministic tasks — and **accuracy 50% while stating
|
|
8
|
+
91% confidence** on synthetic email triage ([Assay research #1/#2](https://github.com/chunxiaoxx/nautilus-compass/discussions/59)).
|
|
9
|
+
Same model. Same day. The number you actually care about is the one for *your* domain,
|
|
10
|
+
and nobody has measured it yet — including the vendor.
|
|
11
|
+
|
|
12
|
+
`jev-trust` wraps the Jev API and measures it in *your* domain while you work:
|
|
13
|
+
|
|
14
|
+
- **Every call logged** — append-only JSONL: question, answer, stated confidence, effective confidence, usage, timestamp.
|
|
15
|
+
- **Outcomes fed back** — as ground truth arrives, `record_outcome()` builds your domain's calibration record: accuracy, Brier, ECE, and calibration currency **C = 1 − ECE**.
|
|
16
|
+
- **Effective confidence on every answer** — `r.effective_confidence` = what the stated confidence is *worth* here so far (observed accuracy of the matching confidence bin, ≥5 samples; else C-adjusted; else `None` = insufficient evidence).
|
|
17
|
+
- **Overconfidence alerts** — a callback fires when a decision is confident enough to act on but the domain hasn't earned that confidence yet.
|
|
18
|
+
- **Signed evidence** — one call signs the whole log (ed25519). Publish log + sig + pubkey; anyone — including [Nautilus Assay](https://compass.nautilus.social/wall.html) — can independently recompute your numbers.
|
|
19
|
+
|
|
20
|
+
Zero dependencies. Pure stdlib. Python ≥ 3.8.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install jev-trust
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from jev_trust import TrustedJev
|
|
32
|
+
|
|
33
|
+
jev = TrustedJev(api_key=API_KEY, domain="email-triage")
|
|
34
|
+
|
|
35
|
+
# decide() never lets unexamined confidence through:
|
|
36
|
+
r = jev.decide(state, {"q": {"type": "choice", "instructions": "...",
|
|
37
|
+
"criteria": {...}}})["q"]
|
|
38
|
+
|
|
39
|
+
r.decision # 'alpha'
|
|
40
|
+
r.stated_confidence # 0.91 <- what Jev claims
|
|
41
|
+
r.effective_confidence # 0.50 <- what 0.91 is worth in YOUR domain so far
|
|
42
|
+
r.basis # 'bin_observed' | 'C_adjusted' | 'insufficient_n'
|
|
43
|
+
r.domain_verdict # FACE_VALUE | DISCOUNT | DOWNGRADE | UNVERIFIED
|
|
44
|
+
|
|
45
|
+
# feed ground truth back as it arrives:
|
|
46
|
+
jev.record_outcome("q", truth)
|
|
47
|
+
|
|
48
|
+
# your domain's running scorecard:
|
|
49
|
+
jev.stats()
|
|
50
|
+
# {'n_outcomes': 42, 'accuracy': 0.52, 'brier': 0.41, 'ece': 0.39,
|
|
51
|
+
# 'C': 0.61, 'verdict': 'DISCOUNT', ...}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## The alert that pays for itself
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
def on_overconfidence(r):
|
|
58
|
+
# fired when stated >= 0.90 but the domain hasn't earned FACE_VALUE
|
|
59
|
+
log.warning(f"{r.qid}: Jev says {r.stated_confidence}, "
|
|
60
|
+
f"worth {r.effective_confidence} here — route to human?")
|
|
61
|
+
|
|
62
|
+
jev = TrustedJev(api_key=API_KEY, domain="fraud-flag",
|
|
63
|
+
alert_confidence=0.90, on_overconfidence=on_overconfidence)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Verdicts (Assay calibration-currency reading levels)
|
|
67
|
+
|
|
68
|
+
| Verdict | Condition | Reading |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `UNVERIFIED` | < 20 outcomes in domain | stated confidence is an unbacked claim |
|
|
71
|
+
| `FACE_VALUE` | C ≥ 0.80 | use stated confidence as-is |
|
|
72
|
+
| `DISCOUNT` | 0.50 ≤ C < 0.80 | multiply trust by C |
|
|
73
|
+
| `DOWNGRADE` | C < 0.50 | route to human review |
|
|
74
|
+
|
|
75
|
+
The library ships with Assay's published reference rates (informational — your
|
|
76
|
+
verdict is always computed from *your* outcomes):
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
jev_trust.ASSAY_REFERENCE_RATES
|
|
80
|
+
# {'closed-deterministic': {'C': 0.959, ...}, # n=240, ECE 0.041
|
|
81
|
+
# 'adversarial-stress': {'C': 0.988, ...}, # n=200, ECE 0.012
|
|
82
|
+
# 'synthetic-email-choice': {'C': 0.086, ...}} # acc 50% @ conf 91%
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Signed evidence — make your numbers independently checkable
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
sig_path = jev.sign_log() # writes <log>.jsonl.sig
|
|
89
|
+
jev.keys.pub_hex # publish this next to the log
|
|
90
|
+
|
|
91
|
+
# anyone can verify:
|
|
92
|
+
from jev_trust import verify_log
|
|
93
|
+
verify_log("session.jsonl", "session.jsonl.sig", pub_hex) # -> VALID
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Same canonical-JSON + ed25519 scheme as
|
|
97
|
+
[assay-verify](https://pypi.org/project/assay-verify/) — a signed `jev-trust`
|
|
98
|
+
log is directly submittable to Nautilus Assay for independent recomputation
|
|
99
|
+
(the evidence format behind their [Domain Calibration Reports](https://compass.nautilus.social/dcr.html)).
|
|
100
|
+
|
|
101
|
+
## Why this exists
|
|
102
|
+
|
|
103
|
+
Nautilus Assay independently verifies AI-agent performance claims. Their two
|
|
104
|
+
public Jev studies found: overall accuracy 92.2% / Brier 0.048 on closed tasks
|
|
105
|
+
(good), but domain calibration collapses on distribution shift — 50% accuracy
|
|
106
|
+
at 91% stated confidence in one synthetic triage domain. Vendor benchmarks
|
|
107
|
+
can't see your domain. `jev-trust` is the always-on instrument that can.
|
|
108
|
+
|
|
109
|
+
Methodology: ECE = equal-width 10-bin top-label; Brier = mean (1 − p_true)²
|
|
110
|
+
(binary-identical to the standard (p − y)² form). Identical formulas to Assay's
|
|
111
|
+
published verification code.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT. © 2026 Nautilus Assay.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""jev-trust · trust middleware for Jev decision APIs.
|
|
3
|
+
|
|
4
|
+
Three lines::
|
|
5
|
+
|
|
6
|
+
from jev_trust import TrustedJev
|
|
7
|
+
|
|
8
|
+
jev = TrustedJev(api_key=..., domain="email-triage")
|
|
9
|
+
r = jev.decide(state, {"q": q})["q"]
|
|
10
|
+
r.stated_confidence, r.effective_confidence, r.domain_verdict
|
|
11
|
+
|
|
12
|
+
Every call is logged; record outcomes and the session measures how much
|
|
13
|
+
Jev's stated confidence is actually worth in YOUR domain (accuracy, Brier,
|
|
14
|
+
ECE, calibration currency C = 1 - ECE). Logs sign with ed25519 for
|
|
15
|
+
independent recomputation (Assay-compatible).
|
|
16
|
+
"""
|
|
17
|
+
from .calib import (CalibrationState, Prediction, ece_toplabel, brier,
|
|
18
|
+
verdict_for, effective_confidence,
|
|
19
|
+
FACE_VALUE, DISCOUNT, DOWNGRADE, UNVERIFIED)
|
|
20
|
+
from .client import JevClient, JevAPIError, top_label, correctness
|
|
21
|
+
from .receipt import KeyPair, ReceiptResult, verify_log
|
|
22
|
+
from .trust import TrustedJev, TrustResult, ASSAY_REFERENCE_RATES
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"TrustedJev", "TrustResult", "ASSAY_REFERENCE_RATES",
|
|
27
|
+
"CalibrationState", "Prediction", "ece_toplabel", "brier",
|
|
28
|
+
"verdict_for", "effective_confidence",
|
|
29
|
+
"FACE_VALUE", "DISCOUNT", "DOWNGRADE", "UNVERIFIED",
|
|
30
|
+
"JevClient", "JevAPIError", "top_label", "correctness",
|
|
31
|
+
"KeyPair", "ReceiptResult", "verify_log",
|
|
32
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Runtime calibration tracking — the same math Assay uses (ECE top-label, Brier).
|
|
3
|
+
|
|
4
|
+
ECE = sum over equal-width confidence bins of (bin share) * |bin accuracy - bin confidence|.
|
|
5
|
+
For binary (noul) questions the top-label confidence is max(p, 1-p); for choice
|
|
6
|
+
questions it is the probability of the chosen option. Identical formulas to
|
|
7
|
+
nautilus-compass verifypack calibration checks (cross-validated against
|
|
8
|
+
sklearn-style references in Assay research #1/#2).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
DEFAULT_BINS = 10
|
|
16
|
+
MIN_BIN_SAMPLES = 5 # below this a bin's observed accuracy is not trustworthy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Prediction:
|
|
21
|
+
"""One recorded decision/outcome pair."""
|
|
22
|
+
qid: str
|
|
23
|
+
stated_confidence: float # top-label confidence the model claimed
|
|
24
|
+
correct: int # 1 / 0 — was the top-label decision right
|
|
25
|
+
p_true: float # probability the model assigned to the TRUE outcome
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _top_label_binary(p: float) -> float:
|
|
29
|
+
"""Jev noul answers state P(yes); top-label confidence = max(p, 1-p)."""
|
|
30
|
+
return max(p, 1.0 - p)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def brier(preds: List[Prediction]) -> float:
|
|
34
|
+
"""mean((1 - p_true)^2) — probability mass the model denied the truth.
|
|
35
|
+
For binary questions identical to the standard (p - y)^2 form."""
|
|
36
|
+
if not preds:
|
|
37
|
+
raise ValueError("no predictions")
|
|
38
|
+
return round(sum((1.0 - pr.p_true) ** 2 for pr in preds) / len(preds), 8)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _bin_of(conf: float, nbins: int) -> int:
|
|
42
|
+
return min(int(conf * nbins), nbins - 1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ece_toplabel(preds: List[Prediction], nbins: int = DEFAULT_BINS) -> float:
|
|
46
|
+
"""Equal-width binned top-label ECE."""
|
|
47
|
+
if not preds:
|
|
48
|
+
raise ValueError("no predictions")
|
|
49
|
+
bins: dict = {}
|
|
50
|
+
for pr in preds:
|
|
51
|
+
b = _bin_of(pr.stated_confidence, nbins)
|
|
52
|
+
acc_sum, conf_sum, cnt = bins.get(b, (0.0, 0.0, 0))
|
|
53
|
+
bins[b] = (acc_sum + pr.correct, conf_sum + pr.stated_confidence, cnt + 1)
|
|
54
|
+
total = len(preds)
|
|
55
|
+
ece = 0.0
|
|
56
|
+
for acc_sum, conf_sum, cnt in bins.values():
|
|
57
|
+
ece += (cnt / total) * abs(acc_sum / cnt - conf_sum / cnt)
|
|
58
|
+
return round(ece, 8)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CalibrationState:
|
|
63
|
+
"""Accumulated calibration evidence for one domain."""
|
|
64
|
+
preds: List[Prediction] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
def add(self, pred: Prediction) -> "CalibrationState":
|
|
67
|
+
self.preds.append(pred)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def n(self) -> int:
|
|
72
|
+
return len(self.preds)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def accuracy(self) -> Optional[float]:
|
|
76
|
+
if not self.preds:
|
|
77
|
+
return None
|
|
78
|
+
return round(sum(p.correct for p in self.preds) / len(self.preds), 4)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def ece(self) -> Optional[float]:
|
|
82
|
+
if not self.preds:
|
|
83
|
+
return None
|
|
84
|
+
return ece_toplabel(self.preds)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def brier(self) -> Optional[float]:
|
|
88
|
+
if not self.preds:
|
|
89
|
+
return None
|
|
90
|
+
return brier(self.preds)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def C(self) -> Optional[float]:
|
|
94
|
+
"""Calibration currency: C = 1 - ECE (Assay definition)."""
|
|
95
|
+
if not self.preds:
|
|
96
|
+
return None
|
|
97
|
+
return round(1.0 - self.ece, 4)
|
|
98
|
+
|
|
99
|
+
def bin_accuracy(self, conf: float, nbins: int = DEFAULT_BINS,
|
|
100
|
+
min_samples: int = MIN_BIN_SAMPLES) -> Optional[float]:
|
|
101
|
+
"""Observed accuracy among past decisions that stated confidence in the
|
|
102
|
+
same bin — 'what is this confidence worth in my domain'. None if the bin
|
|
103
|
+
has too few samples to say anything."""
|
|
104
|
+
b = _bin_of(conf, nbins)
|
|
105
|
+
same = [p.correct for p in self.preds if _bin_of(p.stated_confidence, nbins) == b]
|
|
106
|
+
if len(same) < min_samples:
|
|
107
|
+
return None
|
|
108
|
+
return round(sum(same) / len(same), 4)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── trust verdicts (Assay calibration-currency reading levels) ────────────
|
|
112
|
+
|
|
113
|
+
FACE_VALUE_C = 0.80
|
|
114
|
+
DISCOUNT_C = 0.50
|
|
115
|
+
|
|
116
|
+
FACE_VALUE = "FACE_VALUE" # C >= 0.80: stated confidence may be used at face value
|
|
117
|
+
DISCOUNT = "DISCOUNT" # 0.50 <= C < 0.80: trust stated confidence times C
|
|
118
|
+
DOWNGRADE = "DOWNGRADE" # C < 0.50: route to human review
|
|
119
|
+
UNVERIFIED = "UNVERIFIED" # not enough measured evidence in this domain yet
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def verdict_for(C: Optional[float], n: int, min_n: int = 20) -> str:
|
|
123
|
+
"""Verdict from measured calibration currency. Needs min_n outcomes before
|
|
124
|
+
any 'verified' verdict is issued — below that everything is UNVERIFIED
|
|
125
|
+
(the same discipline Assay applies to its own numbers)."""
|
|
126
|
+
if C is None or n < min_n:
|
|
127
|
+
return UNVERIFIED
|
|
128
|
+
if C >= FACE_VALUE_C:
|
|
129
|
+
return FACE_VALUE
|
|
130
|
+
if C >= DISCOUNT_C:
|
|
131
|
+
return DISCOUNT
|
|
132
|
+
return DOWNGRADE
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def effective_confidence(state: CalibrationState, stated: float) -> Tuple[Optional[float], str]:
|
|
136
|
+
"""How much a stated confidence is worth in this domain, given evidence.
|
|
137
|
+
|
|
138
|
+
Priority: (1) observed accuracy of the matching confidence bin (>=5 samples);
|
|
139
|
+
(2) binless fallback: stated confidence adjusted by measured C; (3) None —
|
|
140
|
+
insufficient evidence, caller must fall back to its own policy."""
|
|
141
|
+
if state.n == 0:
|
|
142
|
+
return None, UNVERIFIED
|
|
143
|
+
bin_acc = state.bin_accuracy(stated)
|
|
144
|
+
if bin_acc is not None:
|
|
145
|
+
return bin_acc, "bin_observed"
|
|
146
|
+
if state.n >= 20:
|
|
147
|
+
return round(0.5 + state.C * (stated - 0.5), 4), "C_adjusted"
|
|
148
|
+
return None, "insufficient_n"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Minimal Jev API client (TypeSafe System One endpoint), pure stdlib.
|
|
3
|
+
|
|
4
|
+
The transport is injectable so tests run offline and callers can plug in
|
|
5
|
+
their own HTTP stack / mocks.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import urllib.request
|
|
12
|
+
from typing import Callable, Dict, Optional
|
|
13
|
+
|
|
14
|
+
DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
|
|
15
|
+
DEFAULT_MODEL = "jev-latest"
|
|
16
|
+
DEFAULT_TIMEOUT = 60
|
|
17
|
+
|
|
18
|
+
Transport = Callable[[str, bytes, Dict[str, str]], dict]
|
|
19
|
+
"""transport(url, body_bytes, headers) -> parsed json response dict"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _urllib_transport(url: str, body: bytes, headers: Dict[str, str]) -> dict:
|
|
23
|
+
req = urllib.request.Request(url, data=body, method="POST", headers=headers)
|
|
24
|
+
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT) as r:
|
|
25
|
+
return json.loads(r.read())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JevClient:
|
|
29
|
+
def __init__(self, api_key: str, endpoint: str = DEFAULT_ENDPOINT,
|
|
30
|
+
model: str = DEFAULT_MODEL, transport: Optional[Transport] = None):
|
|
31
|
+
self.api_key = api_key
|
|
32
|
+
self.endpoint = endpoint
|
|
33
|
+
self.model = model
|
|
34
|
+
self._transport = transport or _urllib_transport
|
|
35
|
+
self.calls = 0
|
|
36
|
+
self.errors = 0
|
|
37
|
+
|
|
38
|
+
def decide(self, state: dict, questions: dict, retries: int = 2,
|
|
39
|
+
retry_delay: float = 1.0) -> dict:
|
|
40
|
+
"""Call Jev once. questions = {qid: {type: noul|choice|score, ...}}.
|
|
41
|
+
Returns the raw response dict {answers: {...}, usage: {...}}.
|
|
42
|
+
Transient errors are retried; content is never retried (no fishing
|
|
43
|
+
for better answers — Assay discipline)."""
|
|
44
|
+
body = json.dumps({"model": self.model, "state": state,
|
|
45
|
+
"questions": questions}).encode()
|
|
46
|
+
headers = {"Authorization": "Bearer " + self.api_key,
|
|
47
|
+
"Content-Type": "application/json"}
|
|
48
|
+
last_err = None
|
|
49
|
+
for attempt in range(retries + 1):
|
|
50
|
+
self.calls += 1
|
|
51
|
+
try:
|
|
52
|
+
return self._transport(self.endpoint, body, headers)
|
|
53
|
+
except Exception as e: # network/5xx class only reaches here
|
|
54
|
+
self.errors += 1
|
|
55
|
+
last_err = e
|
|
56
|
+
if attempt < retries:
|
|
57
|
+
time.sleep(retry_delay * (attempt + 1))
|
|
58
|
+
raise JevAPIError(f"jev api failed after {retries + 1} attempts: {last_err}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JevAPIError(RuntimeError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── answer normalization ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def top_label(answer: dict) -> dict:
|
|
68
|
+
"""Normalize a Jev answer into {decision, stated_confidence, probabilities}.
|
|
69
|
+
|
|
70
|
+
- noul: {noul: 0.93, type: noul} -> decision yes/no
|
|
71
|
+
- choice: {choice: 'alpha', probabilities: {...}} -> decision 'alpha'
|
|
72
|
+
- score: {score: 0.8, type: score} -> value 0.8, confidence 1.0
|
|
73
|
+
"""
|
|
74
|
+
t = answer.get("type")
|
|
75
|
+
if t == "noul":
|
|
76
|
+
p = float(answer["noul"])
|
|
77
|
+
decision = "yes" if p >= 0.5 else "no"
|
|
78
|
+
return {"decision": decision, "stated_confidence": max(p, 1.0 - p),
|
|
79
|
+
"probabilities": {"yes": p, "no": 1.0 - p}}
|
|
80
|
+
if t == "choice":
|
|
81
|
+
probs = dict(answer.get("probabilities") or {})
|
|
82
|
+
decision = answer.get("choice")
|
|
83
|
+
stated = answer.get("confidence")
|
|
84
|
+
if stated is None:
|
|
85
|
+
stated = max(probs.values()) if probs else 1.0
|
|
86
|
+
return {"decision": decision, "stated_confidence": float(stated),
|
|
87
|
+
"probabilities": probs}
|
|
88
|
+
if t == "score":
|
|
89
|
+
return {"decision": float(answer["score"]), "stated_confidence": 1.0,
|
|
90
|
+
"probabilities": None}
|
|
91
|
+
raise ValueError(f"unknown answer type: {t!r}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def correctness(top: dict, truth) -> int:
|
|
95
|
+
"""Was the top-label decision correct, given ground truth?
|
|
96
|
+
noul: truth in {0,1} vs decision yes/no; choice: string equality.
|
|
97
|
+
score answers carry no notion of correctness — raise rather than guess."""
|
|
98
|
+
decision = top["decision"]
|
|
99
|
+
if isinstance(decision, float):
|
|
100
|
+
raise ValueError("score answers have no top-label correctness")
|
|
101
|
+
if decision in ("yes", "no"):
|
|
102
|
+
yes = {1: "yes", 0: "no", True: "yes", False: "no"}.get(truth)
|
|
103
|
+
if yes is None:
|
|
104
|
+
raise ValueError(f"bad truth for noul answer: {truth!r}")
|
|
105
|
+
return 1 if decision == yes else 0
|
|
106
|
+
return 1 if str(decision) == str(truth) else 0
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ed25519 (RFC 8032) — vendored reference implementation.
|
|
3
|
+
|
|
4
|
+
Same source as assay-verify (sdks/assay-verify) and nautilus-compass
|
|
5
|
+
tools/verifypack; the three implementations are cross-validated (VALID
|
|
6
|
+
both ways, 2026-09-19). Pure python, zero dependencies.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import secrets
|
|
12
|
+
|
|
13
|
+
_p = 2**255 - 19
|
|
14
|
+
_q = 2**252 + 27742317777372353535851937790883648493
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _inv(x: int) -> int:
|
|
18
|
+
return pow(x, _p - 2, _p)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_d = -121665 * _inv(121666) % _p
|
|
22
|
+
_I = pow(2, (_p - 1) // 4, _p)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _xrecover(y: int) -> int:
|
|
26
|
+
xx = (y * y - 1) * _inv(_d * y * y + 1)
|
|
27
|
+
x = pow(xx, (_p + 3) // 8, _p)
|
|
28
|
+
if (x * x - xx) % _p != 0:
|
|
29
|
+
x = x * _I % _p
|
|
30
|
+
if x % 2 != 0:
|
|
31
|
+
x = _p - x
|
|
32
|
+
return x
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_By = 4 * _inv(5) % _p
|
|
36
|
+
_Bx = _xrecover(_By)
|
|
37
|
+
_B = (_Bx % _p, _By % _p, 1, _Bx * _By % _p)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _edwards_add(P, Q):
|
|
41
|
+
x1, y1, z1, t1 = P
|
|
42
|
+
x2, y2, z2, t2 = Q
|
|
43
|
+
a = (y1 - x1) * (y2 - x2) % _p
|
|
44
|
+
b = (y1 + x1) * (y2 + x2) % _p
|
|
45
|
+
c = t1 * 2 * _d * t2 % _p
|
|
46
|
+
dd = z1 * 2 * z2 % _p
|
|
47
|
+
e, f, g, h = b - a, dd - c, dd + c, b + a
|
|
48
|
+
return (e * f % _p, g * h % _p, f * g % _p, e * h % _p)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _scalarmult(P, e):
|
|
52
|
+
if e == 0:
|
|
53
|
+
return (0, 1, 1, 0)
|
|
54
|
+
Q = _scalarmult(P, e // 2)
|
|
55
|
+
Q = _edwards_add(Q, Q)
|
|
56
|
+
if e & 1:
|
|
57
|
+
Q = _edwards_add(Q, P)
|
|
58
|
+
return Q
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _point_decompress(s: bytes):
|
|
62
|
+
if len(s) != 32:
|
|
63
|
+
raise ValueError("bad point")
|
|
64
|
+
y = int.from_bytes(s, "little")
|
|
65
|
+
sign = y >> 255
|
|
66
|
+
y &= (1 << 255) - 1
|
|
67
|
+
x = _xrecover(y)
|
|
68
|
+
if x & 1 != sign:
|
|
69
|
+
x = _p - x
|
|
70
|
+
P = (x, y, 1, x * y % _p)
|
|
71
|
+
if not (0 <= x < _p and 0 <= y < _p):
|
|
72
|
+
raise ValueError("out of range")
|
|
73
|
+
return P
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _sha512(m: bytes) -> bytes:
|
|
77
|
+
return hashlib.sha512(m).digest()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _sha512_modq(m: bytes) -> int:
|
|
81
|
+
return int.from_bytes(_sha512(m), "little") % _q
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _encode_point(P) -> bytes:
|
|
85
|
+
x, y, z, _ = P
|
|
86
|
+
zinv = _inv(z)
|
|
87
|
+
x = x * zinv % _p
|
|
88
|
+
y = y * zinv % _p
|
|
89
|
+
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sign(msg: bytes, seed: bytes) -> bytes:
|
|
93
|
+
h = _sha512(seed)
|
|
94
|
+
a = int.from_bytes(h[:32], "little")
|
|
95
|
+
a &= (1 << 254) - 8
|
|
96
|
+
a |= 1 << 254
|
|
97
|
+
pub = _encode_point(_scalarmult(_B, a))
|
|
98
|
+
prefix = h[32:]
|
|
99
|
+
r = _sha512_modq(prefix + msg)
|
|
100
|
+
Rs = _encode_point(_scalarmult(_B, r))
|
|
101
|
+
s = (r + _sha512_modq(Rs + pub + msg) * a) % _q
|
|
102
|
+
return Rs + s.to_bytes(32, "little")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def verify(public: bytes, msg: bytes, signature: bytes) -> bool:
|
|
106
|
+
if len(public) != 32 or len(signature) != 64:
|
|
107
|
+
return False
|
|
108
|
+
try:
|
|
109
|
+
A = _point_decompress(public)
|
|
110
|
+
R = _point_decompress(signature[:32])
|
|
111
|
+
except ValueError:
|
|
112
|
+
return False
|
|
113
|
+
S = int.from_bytes(signature[32:], "little")
|
|
114
|
+
h = _sha512_modq(signature[:32] + public + msg)
|
|
115
|
+
sB = _scalarmult(_B, S)
|
|
116
|
+
hA = _scalarmult(A, h)
|
|
117
|
+
RhA = _edwards_add(R, hA)
|
|
118
|
+
return (sB[1] * RhA[2] - RhA[1] * sB[2]) % _p == 0 \
|
|
119
|
+
and (sB[0] * RhA[2] - RhA[0] * sB[2]) % _p == 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def keypair() -> tuple:
|
|
123
|
+
seed = secrets.token_bytes(32)
|
|
124
|
+
h = _sha512(seed)
|
|
125
|
+
a = int.from_bytes(h[:32], "little")
|
|
126
|
+
a &= (1 << 254) - 8
|
|
127
|
+
a |= 1 << 254
|
|
128
|
+
pub = _encode_point(_scalarmult(_B, a))
|
|
129
|
+
return seed, pub
|