gonogo-eval 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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ examples/data/
11
+ examples/banking77_report.html
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Dominguez
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,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: gonogo-eval
3
+ Version: 0.1.0
4
+ Summary: Decide whether an agent is good enough to ship. Honest statistics for pilot-scale case sets.
5
+ Project-URL: Homepage, https://github.com/keppy/gonogo
6
+ Project-URL: Source, https://github.com/keppy/gonogo
7
+ Author: James Dominguez
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,calibration,evaluation,llm,selective-prediction
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.10
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # gonogo
22
+
23
+ **47 out of 50 is not 94%.** It lands somewhere between 84% and 98%, so if you were aiming at 90%, you can't yet say you got there.
24
+
25
+ Feed `gonogo` your agent and your real cases. Back comes a decision: ship it, ship it behind a human-review threshold, or walk away. When the honest answer is "you don't have enough cases to know," it says that instead of guessing.
26
+
27
+ This is deliberately **not** another eval framework — several good ones already exist. What none of them do is convert a score into a deployment decision you can defend at the sample sizes pilots actually run: forty to a hundred cases, not ten thousand.
28
+
29
+ ```bash
30
+ pip install gonogo-eval
31
+ ```
32
+
33
+ Installs as `gonogo-eval` (the plain `gonogo` name on PyPI belongs to an unrelated project); imports as `gonogo`.
34
+
35
+ ## The idea
36
+
37
+ Evaluate an agent on a small set of real cases and three things go wrong.
38
+
39
+ **The point estimate flatters you.** 47/50 reads as 94%. The 95% interval is [83.8%, 97.9%]. Most tools print the 94% and stop there.
40
+
41
+ **Accuracy isn't what anyone's asking.** The real question is how much work you can hand over at 98% precision, and how many cases end up on someone's desk. Answering it means a risk–coverage curve over an abstention threshold. Almost nothing computes one.
42
+
43
+ **A dashboard isn't a decision.** Somebody still has to say ship or don't ship, and that call ought to fall out of the numbers rather than out of a meeting.
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from gonogo import Case, evaluate
49
+ from gonogo.scoring import fields
50
+
51
+ cases = Case.from_jsonl("invoices.jsonl") # your real cases
52
+
53
+ def agent(case):
54
+ result = my_pipeline(case.input)
55
+ return result.data, result.confidence # confidence is optional but unlocks a lot
56
+
57
+ report = evaluate(agent, cases, scorer=fields(["vendor", "total"]), target=0.95)
58
+ print(report.markdown())
59
+ ```
60
+
61
+ Your agent is any callable. No base class, no decorator, no framework to adopt. Return a bare value, a `(value, confidence)` tuple, or a `Prediction` when you want to signal abstention.
62
+
63
+ ## What it tells you
64
+
65
+ Five verdicts, and only two of them mean ship:
66
+
67
+ | Verdict | Meaning |
68
+ | --- | --- |
69
+ | `AUTOMATE` | The pass rate's lower bound clears your target. Ship it. |
70
+ | `AUTOMATE WITH REVIEW` | Not good enough overall, but a confident subset is. Ship behind a threshold, route the rest to a person. |
71
+ | `ASSIST ONLY` | Useful as a draft generator, not as an unattended step. |
72
+ | `DO NOT AUTOMATE` | Not a fit for this workflow as scoped. |
73
+ | `INSUFFICIENT EVIDENCE` | The estimate looks good but your sample can't support the claim. Here's roughly how many cases you'd need. |
74
+
75
+ That last one is the whole reason this exists. It's the verdict an honest consultant gives and a dashboard never does.
76
+
77
+ ## Why confidence matters
78
+
79
+ If your agent reports a per-case confidence, `gonogo` finds the abstention threshold that maximizes how much you can automate while keeping precision's *lower bound* above your target, and reports the whole curve so you can see the tradeoff:
80
+
81
+ | Confidence floor | Handled | Precision | To review |
82
+ | --- | --- | --- | --- |
83
+ | 0.36 | 100% | 88.3% [77.8%, 94.2%] | 0 |
84
+ | 0.70 | 87% | 94.2% [84.4%, 98.0%] | 8 |
85
+ | 0.85 | 82% | 98.0% [89.3%, 99.6%] | 11 |
86
+ | 0.89 | 57% | 100.0% [89.8%, 100.0%] | 26 |
87
+ | 0.99 | 2% | 100.0% [20.7%, 100.0%] | 59 |
88
+
89
+ That last row is why it uses the lower bound and not the point estimate. 100% precision on a single case is not an operating point, and the interval says so.
90
+
91
+ Note what the table above actually proves: against a 95% target, *no* threshold works here. The 0.85 row looks great at 98.0% until you read its lower bound of 89.3%. So the verdict is `ASSIST ONLY`, not a ship — which is the answer you want before you wire it into production, not after.
92
+
93
+ It also checks whether your confidence means anything. Expected calibration error above 0.15 and the harness refuses to recommend a threshold at all — thresholding on a number that doesn't track correctness is theater.
94
+
95
+ ## A real measurement
96
+
97
+ `examples/banking77_routing.py` runs against [Banking77](https://github.com/PolyAI-LDN/task-specific-datasets) — 13,083 genuine retail-bank customer messages across 77 intents. The agent is an ordinary TF-IDF nearest-centroid baseline, stdlib only, no model API.
98
+
99
+ On a pilot-sized sample of 250 cases against a 95% target:
100
+
101
+ ```
102
+ ASSIST ONLY: Use it to draft, keep a human on every case.
103
+ Pass rate 77.2% [71.6%, 82.0%] (full 3,080-case split: 80.7% [79.3%, 82.1%])
104
+ Calibration error 0.42 (ranks cases, scale unreliable)
105
+ ```
106
+
107
+ The calibration table is the interesting part:
108
+
109
+ | Stated confidence | Cases | Mean confidence | Actual accuracy |
110
+ | --- | --- | --- | --- |
111
+ | 0.0–0.2 | 83 | 0.11 | 61% |
112
+ | 0.2–0.4 | 74 | 0.30 | 78% |
113
+ | 0.4–0.6 | 50 | 0.51 | 84% |
114
+ | 0.6–0.8 | 30 | 0.70 | 97% |
115
+ | 0.8–1.0 | 13 | 0.87 | 100% |
116
+
117
+ That model is *badly* miscalibrated — it says 0.11 and is right 61% of the time — while still ranking cases almost perfectly. Those are two different properties, and conflating them is a common way to throw away a usable signal. `gonogo` reports the calibration error, says the number isn't a probability, and still measures precision at each cut point empirically, because that measurement doesn't depend on the scale being meaningful.
118
+
119
+ ## On LLM-as-judge
120
+
121
+ `scoring.judge` takes any `complete(prompt) -> str` callable, so there's no provider SDK in the dependency tree.
122
+
123
+ An unvalidated judge is the most common silent failure in agent evaluation. Hand-label a subset and run `validate_judge(judge_labels, human_labels)`:
124
+
125
+ ```
126
+ judge NOT USABLE: 90% agreement, kappa 0.00 on 30 hand-labelled cases
127
+ (kappa 0.00 is below 0.60 and it passes cases you failed; fix the rubric)
128
+ ```
129
+
130
+ That's the trap. A judge that rubber-stamps everything scores 90% agreement on a set that's 90% passes, and carries no information whatsoever. Kappa is the gate, not agreement.
131
+
132
+ ## Reports
133
+
134
+ `report.markdown()` for a terminal or a PR comment; `report.html()` for a self-contained styled page with no external assets, which you can hand to whoever signs off; `report.to_dict()` for JSON.
135
+
136
+ ## Non-goals
137
+
138
+ This is a reference implementation, around 800 lines, readable in one sitting. It will not grow into:
139
+
140
+ - a hosted service or dashboard
141
+ - tracing / observability
142
+ - prompt management or versioning
143
+ - an agent framework
144
+ - a public leaderboard
145
+
146
+ If you want those, use one of the platforms. This does one thing.
147
+
148
+ ## Run the example
149
+
150
+ ```bash
151
+ python examples/invoice_extraction.py
152
+ ```
153
+
154
+ Sixty simulated invoices, an agent that's good but not perfect, no API key required. Real output:
155
+
156
+ ```
157
+ # Score report: Extract fields from invoice
158
+
159
+ **ASSIST ONLY**: Use it to draft, keep a human on every case.
160
+
161
+ Pass rate 88.3% [77.8%, 94.2%] is well short of the 95% target and no
162
+ confident subset reaches it; useful as a draft-generator, not as an
163
+ unattended step.
164
+
165
+ | Cases evaluated | 60 |
166
+ | Passed | 53 |
167
+ | Pass rate | 88.3% [77.8%, 94.2%] |
168
+ | Target | 95% |
169
+ | Calibration error | 0.11 (usable) |
170
+ ```
171
+
172
+ The calibration table in the full report is worth a look too: this agent is
173
+ well calibrated above 0.8 (stated 0.92, actual 98%) and badly calibrated in
174
+ the 0.6–0.8 band (stated 0.68, actual 33%). That's the kind of thing you want
175
+ to know before you pick a threshold.
176
+
177
+ ## License
178
+
179
+ MIT
@@ -0,0 +1,159 @@
1
+ # gonogo
2
+
3
+ **47 out of 50 is not 94%.** It lands somewhere between 84% and 98%, so if you were aiming at 90%, you can't yet say you got there.
4
+
5
+ Feed `gonogo` your agent and your real cases. Back comes a decision: ship it, ship it behind a human-review threshold, or walk away. When the honest answer is "you don't have enough cases to know," it says that instead of guessing.
6
+
7
+ This is deliberately **not** another eval framework — several good ones already exist. What none of them do is convert a score into a deployment decision you can defend at the sample sizes pilots actually run: forty to a hundred cases, not ten thousand.
8
+
9
+ ```bash
10
+ pip install gonogo-eval
11
+ ```
12
+
13
+ Installs as `gonogo-eval` (the plain `gonogo` name on PyPI belongs to an unrelated project); imports as `gonogo`.
14
+
15
+ ## The idea
16
+
17
+ Evaluate an agent on a small set of real cases and three things go wrong.
18
+
19
+ **The point estimate flatters you.** 47/50 reads as 94%. The 95% interval is [83.8%, 97.9%]. Most tools print the 94% and stop there.
20
+
21
+ **Accuracy isn't what anyone's asking.** The real question is how much work you can hand over at 98% precision, and how many cases end up on someone's desk. Answering it means a risk–coverage curve over an abstention threshold. Almost nothing computes one.
22
+
23
+ **A dashboard isn't a decision.** Somebody still has to say ship or don't ship, and that call ought to fall out of the numbers rather than out of a meeting.
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ from gonogo import Case, evaluate
29
+ from gonogo.scoring import fields
30
+
31
+ cases = Case.from_jsonl("invoices.jsonl") # your real cases
32
+
33
+ def agent(case):
34
+ result = my_pipeline(case.input)
35
+ return result.data, result.confidence # confidence is optional but unlocks a lot
36
+
37
+ report = evaluate(agent, cases, scorer=fields(["vendor", "total"]), target=0.95)
38
+ print(report.markdown())
39
+ ```
40
+
41
+ Your agent is any callable. No base class, no decorator, no framework to adopt. Return a bare value, a `(value, confidence)` tuple, or a `Prediction` when you want to signal abstention.
42
+
43
+ ## What it tells you
44
+
45
+ Five verdicts, and only two of them mean ship:
46
+
47
+ | Verdict | Meaning |
48
+ | --- | --- |
49
+ | `AUTOMATE` | The pass rate's lower bound clears your target. Ship it. |
50
+ | `AUTOMATE WITH REVIEW` | Not good enough overall, but a confident subset is. Ship behind a threshold, route the rest to a person. |
51
+ | `ASSIST ONLY` | Useful as a draft generator, not as an unattended step. |
52
+ | `DO NOT AUTOMATE` | Not a fit for this workflow as scoped. |
53
+ | `INSUFFICIENT EVIDENCE` | The estimate looks good but your sample can't support the claim. Here's roughly how many cases you'd need. |
54
+
55
+ That last one is the whole reason this exists. It's the verdict an honest consultant gives and a dashboard never does.
56
+
57
+ ## Why confidence matters
58
+
59
+ If your agent reports a per-case confidence, `gonogo` finds the abstention threshold that maximizes how much you can automate while keeping precision's *lower bound* above your target, and reports the whole curve so you can see the tradeoff:
60
+
61
+ | Confidence floor | Handled | Precision | To review |
62
+ | --- | --- | --- | --- |
63
+ | 0.36 | 100% | 88.3% [77.8%, 94.2%] | 0 |
64
+ | 0.70 | 87% | 94.2% [84.4%, 98.0%] | 8 |
65
+ | 0.85 | 82% | 98.0% [89.3%, 99.6%] | 11 |
66
+ | 0.89 | 57% | 100.0% [89.8%, 100.0%] | 26 |
67
+ | 0.99 | 2% | 100.0% [20.7%, 100.0%] | 59 |
68
+
69
+ That last row is why it uses the lower bound and not the point estimate. 100% precision on a single case is not an operating point, and the interval says so.
70
+
71
+ Note what the table above actually proves: against a 95% target, *no* threshold works here. The 0.85 row looks great at 98.0% until you read its lower bound of 89.3%. So the verdict is `ASSIST ONLY`, not a ship — which is the answer you want before you wire it into production, not after.
72
+
73
+ It also checks whether your confidence means anything. Expected calibration error above 0.15 and the harness refuses to recommend a threshold at all — thresholding on a number that doesn't track correctness is theater.
74
+
75
+ ## A real measurement
76
+
77
+ `examples/banking77_routing.py` runs against [Banking77](https://github.com/PolyAI-LDN/task-specific-datasets) — 13,083 genuine retail-bank customer messages across 77 intents. The agent is an ordinary TF-IDF nearest-centroid baseline, stdlib only, no model API.
78
+
79
+ On a pilot-sized sample of 250 cases against a 95% target:
80
+
81
+ ```
82
+ ASSIST ONLY: Use it to draft, keep a human on every case.
83
+ Pass rate 77.2% [71.6%, 82.0%] (full 3,080-case split: 80.7% [79.3%, 82.1%])
84
+ Calibration error 0.42 (ranks cases, scale unreliable)
85
+ ```
86
+
87
+ The calibration table is the interesting part:
88
+
89
+ | Stated confidence | Cases | Mean confidence | Actual accuracy |
90
+ | --- | --- | --- | --- |
91
+ | 0.0–0.2 | 83 | 0.11 | 61% |
92
+ | 0.2–0.4 | 74 | 0.30 | 78% |
93
+ | 0.4–0.6 | 50 | 0.51 | 84% |
94
+ | 0.6–0.8 | 30 | 0.70 | 97% |
95
+ | 0.8–1.0 | 13 | 0.87 | 100% |
96
+
97
+ That model is *badly* miscalibrated — it says 0.11 and is right 61% of the time — while still ranking cases almost perfectly. Those are two different properties, and conflating them is a common way to throw away a usable signal. `gonogo` reports the calibration error, says the number isn't a probability, and still measures precision at each cut point empirically, because that measurement doesn't depend on the scale being meaningful.
98
+
99
+ ## On LLM-as-judge
100
+
101
+ `scoring.judge` takes any `complete(prompt) -> str` callable, so there's no provider SDK in the dependency tree.
102
+
103
+ An unvalidated judge is the most common silent failure in agent evaluation. Hand-label a subset and run `validate_judge(judge_labels, human_labels)`:
104
+
105
+ ```
106
+ judge NOT USABLE: 90% agreement, kappa 0.00 on 30 hand-labelled cases
107
+ (kappa 0.00 is below 0.60 and it passes cases you failed; fix the rubric)
108
+ ```
109
+
110
+ That's the trap. A judge that rubber-stamps everything scores 90% agreement on a set that's 90% passes, and carries no information whatsoever. Kappa is the gate, not agreement.
111
+
112
+ ## Reports
113
+
114
+ `report.markdown()` for a terminal or a PR comment; `report.html()` for a self-contained styled page with no external assets, which you can hand to whoever signs off; `report.to_dict()` for JSON.
115
+
116
+ ## Non-goals
117
+
118
+ This is a reference implementation, around 800 lines, readable in one sitting. It will not grow into:
119
+
120
+ - a hosted service or dashboard
121
+ - tracing / observability
122
+ - prompt management or versioning
123
+ - an agent framework
124
+ - a public leaderboard
125
+
126
+ If you want those, use one of the platforms. This does one thing.
127
+
128
+ ## Run the example
129
+
130
+ ```bash
131
+ python examples/invoice_extraction.py
132
+ ```
133
+
134
+ Sixty simulated invoices, an agent that's good but not perfect, no API key required. Real output:
135
+
136
+ ```
137
+ # Score report: Extract fields from invoice
138
+
139
+ **ASSIST ONLY**: Use it to draft, keep a human on every case.
140
+
141
+ Pass rate 88.3% [77.8%, 94.2%] is well short of the 95% target and no
142
+ confident subset reaches it; useful as a draft-generator, not as an
143
+ unattended step.
144
+
145
+ | Cases evaluated | 60 |
146
+ | Passed | 53 |
147
+ | Pass rate | 88.3% [77.8%, 94.2%] |
148
+ | Target | 95% |
149
+ | Calibration error | 0.11 (usable) |
150
+ ```
151
+
152
+ The calibration table in the full report is worth a look too: this agent is
153
+ well calibrated above 0.8 (stated 0.92, actual 98%) and badly calibrated in
154
+ the 0.6–0.8 band (stated 0.68, actual 33%). That's the kind of thing you want
155
+ to know before you pick a threshold.
156
+
157
+ ## License
158
+
159
+ MIT
@@ -0,0 +1,147 @@
1
+ """A real measurement on a real public dataset.
2
+
3
+ Banking77 is 13,083 genuine customer-service messages from a retail bank,
4
+ labelled into 77 intents. Routing a message to the right queue is exactly the
5
+ kind of workflow a pilot gets scoped around, so it makes an honest stand-in for
6
+ "can we automate our support triage?"
7
+
8
+ The agent here is a deliberately ordinary baseline: TF-IDF nearest-centroid,
9
+ pure stdlib, no model API. That is the point. A plausible-looking baseline is
10
+ what most demos are built on, and the question is whether the harness tells you
11
+ the truth about it.
12
+
13
+ python examples/banking77_routing.py
14
+
15
+ Data is fetched once from the PolyAI repository and cached next to this file.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import csv
21
+ import math
22
+ import random
23
+ import re
24
+ import urllib.request
25
+ from collections import Counter, defaultdict
26
+ from pathlib import Path
27
+
28
+ from gonogo import Case, Prediction, evaluate
29
+ from gonogo.scoring import exact
30
+
31
+ BASE = "https://raw.githubusercontent.com/PolyAI-LDN/task-specific-datasets/master/banking_data"
32
+ CACHE = Path(__file__).parent / "data"
33
+ TOKEN = re.compile(r"[a-z0-9']+")
34
+
35
+ # A pilot ships with a case set a human actually labelled, not a full benchmark
36
+ # split. 250 is a realistic size and it keeps the small-n lesson visible.
37
+ PILOT_SIZE = 250
38
+ SEED = 11
39
+
40
+
41
+ def fetch(name: str) -> list[tuple[str, str]]:
42
+ CACHE.mkdir(exist_ok=True)
43
+ path = CACHE / f"banking77_{name}.csv"
44
+ if not path.exists():
45
+ print(f"downloading banking77 {name} split...")
46
+ urllib.request.urlretrieve(f"{BASE}/{name}.csv", path)
47
+ with path.open(encoding="utf-8", newline="") as fh:
48
+ return [(row["text"], row["category"]) for row in csv.DictReader(fh)]
49
+
50
+
51
+ def tokenize(text: str) -> list[str]:
52
+ return TOKEN.findall(text.lower())
53
+
54
+
55
+ class NearestCentroid:
56
+ """TF-IDF nearest-centroid classifier with a softmax confidence.
57
+
58
+ Confidence is the softmax over centroid similarities. It is a real signal --
59
+ ambiguous messages do score lower -- but it was never calibrated against
60
+ anything, which is exactly the situation most agents are in.
61
+ """
62
+
63
+ def __init__(self, temperature: float = 12.0):
64
+ self.temperature = temperature
65
+ self.idf: dict[str, float] = {}
66
+ self.centroids: dict[str, dict[str, float]] = {}
67
+
68
+ def fit(self, rows: list[tuple[str, str]]) -> "NearestCentroid":
69
+ docs = [tokenize(t) for t, _ in rows]
70
+ df = Counter()
71
+ for tokens in docs:
72
+ df.update(set(tokens))
73
+ n = len(docs)
74
+ self.idf = {term: math.log(n / (1 + count)) + 1.0 for term, count in df.items()}
75
+
76
+ sums: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
77
+ counts: Counter = Counter()
78
+ for tokens, (_, label) in zip(docs, rows):
79
+ for term, weight in self._vector(tokens).items():
80
+ sums[label][term] += weight
81
+ counts[label] += 1
82
+
83
+ for label, vec in sums.items():
84
+ centroid = {t: w / counts[label] for t, w in vec.items()}
85
+ self.centroids[label] = self._normalize(centroid)
86
+ return self
87
+
88
+ def _vector(self, tokens: list[str]) -> dict[str, float]:
89
+ tf = Counter(tokens)
90
+ vec = {t: (1 + math.log(c)) * self.idf.get(t, 1.0) for t, c in tf.items()}
91
+ return self._normalize(vec)
92
+
93
+ @staticmethod
94
+ def _normalize(vec: dict[str, float]) -> dict[str, float]:
95
+ norm = math.sqrt(sum(w * w for w in vec.values()))
96
+ return {t: w / norm for t, w in vec.items()} if norm else vec
97
+
98
+ def predict(self, text: str) -> tuple[str, float]:
99
+ vec = self._vector(tokenize(text))
100
+ scores = {
101
+ label: sum(w * centroid.get(t, 0.0) for t, w in vec.items())
102
+ for label, centroid in self.centroids.items()
103
+ }
104
+ if not scores:
105
+ return "", 0.0
106
+ best = max(scores, key=scores.get)
107
+ top = max(scores.values())
108
+ total = sum(math.exp(self.temperature * (s - top)) for s in scores.values())
109
+ return best, 1.0 / total
110
+
111
+
112
+ def main() -> None:
113
+ train = fetch("train")
114
+ test = fetch("test")
115
+ model = NearestCentroid().fit(train)
116
+
117
+ pilot = random.Random(SEED).sample(test, PILOT_SIZE)
118
+ cases = [
119
+ Case(input=text, expected=label, id=f"msg-{i:04d}")
120
+ for i, (text, label) in enumerate(pilot)
121
+ ]
122
+
123
+ def agent(case: Case) -> Prediction:
124
+ label, confidence = model.predict(case.input)
125
+ return Prediction(output=label, confidence=round(confidence, 3))
126
+
127
+ report = evaluate(
128
+ agent,
129
+ cases,
130
+ scorer=exact(),
131
+ task="Route customer message to the right queue",
132
+ target=0.95,
133
+ )
134
+ print(report.markdown())
135
+
136
+ out = Path(__file__).parent / "banking77_report.html"
137
+ out.write_text(report.html(), encoding="utf-8")
138
+ print(f"\n[html report written to {out}]")
139
+
140
+ # For context: the same agent measured on the full held-out split.
141
+ full = [Case(input=t, expected=l, id=str(i)) for i, (t, l) in enumerate(test)]
142
+ full_report = evaluate(agent, full, scorer=exact(), task="full test split", target=0.95)
143
+ print(f"[full split, n={full_report.n}: {full_report.decision.pass_rate}]")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ main()
@@ -0,0 +1,81 @@
1
+ """A worked example you can run with no API key.
2
+
3
+ Simulates a document-extraction pilot: 60 real-ish invoices, an agent that is
4
+ good but not perfect and reports a confidence, and the resulting decision.
5
+
6
+ python examples/invoice_extraction.py
7
+
8
+ The point of the example is the *shape of the answer*. The agent here gets about
9
+ 88% of cases right, which looks shippable until you see the interval -- and then
10
+ you see that abstaining on its low-confidence cases buys you a defensible
11
+ operating point instead.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import random
17
+
18
+ from gonogo import Case, Prediction, evaluate
19
+ from gonogo.scoring import fields
20
+
21
+ VENDORS = ["Acme Supply", "Northwind Freight", "Globex Paper", "Initech Legal", "Umbrella Labs"]
22
+
23
+
24
+ def build_cases(n: int = 60, seed: int = 7) -> list[Case]:
25
+ """Stand-in for a real case set. In a pilot this is a JSONL file of your invoices."""
26
+ rng = random.Random(seed)
27
+ cases = []
28
+ for i in range(n):
29
+ vendor = rng.choice(VENDORS)
30
+ total = round(rng.uniform(40, 9000), 2)
31
+ # Some invoices are genuinely hard: handwritten, multi-page, or foreign currency.
32
+ hard = rng.random() < 0.25
33
+ cases.append(Case(
34
+ id=f"inv-{i:03d}",
35
+ input={"scan": f"invoice_{i:03d}.pdf", "pages": 3 if hard else 1},
36
+ expected={"vendor": vendor, "total": total, "currency": "USD"},
37
+ metadata={"hard": hard},
38
+ ))
39
+ return cases
40
+
41
+
42
+ def agent(case: Case) -> Prediction:
43
+ """A plausible extraction agent: usually right, shakier on hard documents.
44
+
45
+ Crucially it reports a calibrated-ish confidence, which is what lets the
46
+ harness find an abstention threshold. An agent that always says 0.99 gives
47
+ you nothing to threshold on.
48
+ """
49
+ rng = random.Random(hash(case.id) & 0xFFFF)
50
+ hard = case.metadata.get("hard", False)
51
+ correct = rng.random() > (0.35 if hard else 0.04)
52
+
53
+ expected = case.expected
54
+ if correct:
55
+ output = dict(expected)
56
+ else:
57
+ # Realistic failure modes: wrong total, or a truncated vendor name.
58
+ output = dict(expected)
59
+ if rng.random() < 0.6:
60
+ output["total"] = round(expected["total"] * rng.uniform(1.05, 1.4), 2)
61
+ else:
62
+ output["vendor"] = expected["vendor"].split()[0]
63
+
64
+ confidence = rng.uniform(0.35, 0.75) if hard else rng.uniform(0.85, 0.99)
65
+ return Prediction(output=output, confidence=round(confidence, 2))
66
+
67
+
68
+ def main() -> None:
69
+ cases = build_cases()
70
+ report = evaluate(
71
+ agent,
72
+ cases,
73
+ scorer=fields(required=["vendor", "total", "currency"]),
74
+ task="Extract fields from invoice",
75
+ target=0.95,
76
+ )
77
+ print(report.markdown())
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -0,0 +1,44 @@
1
+ """gonogo -- decide whether an agent is good enough to ship.
2
+
3
+ Not an eval framework. A decision harness for pilot-scale case sets, where the
4
+ honest answer is often "not enough evidence yet" and sometimes "don't automate
5
+ this at all".
6
+ """
7
+
8
+ from .cases import Case, CaseResult, Prediction
9
+ from .decide import Decision, Verdict, decide
10
+ from .evaluate import evaluate
11
+ from .report import Report
12
+ from .scoring import (
13
+ JudgeValidation,
14
+ exact,
15
+ fields,
16
+ judge,
17
+ judge_agreement,
18
+ numeric,
19
+ set_f1,
20
+ validate_judge,
21
+ )
22
+ from .stats import (
23
+ Interval,
24
+ OperatingPoint,
25
+ best_operating_point,
26
+ expected_calibration_error,
27
+ reliability_table,
28
+ required_n,
29
+ risk_coverage,
30
+ wilson,
31
+ )
32
+
33
+ __version__ = "0.1.0"
34
+
35
+ __all__ = [
36
+ "Case", "CaseResult", "Prediction",
37
+ "Decision", "Verdict", "decide",
38
+ "evaluate", "Report",
39
+ "exact", "fields", "judge", "judge_agreement", "numeric", "set_f1",
40
+ "JudgeValidation", "validate_judge",
41
+ "Interval", "OperatingPoint", "best_operating_point",
42
+ "expected_calibration_error", "reliability_table", "required_n",
43
+ "risk_coverage", "wilson",
44
+ ]