aicordon 0.1.0__py3-none-any.whl

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.
aicordon/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """AI Cordon — the shared core of the company's products: finding model, detector contract, CLI.
2
+
3
+ There is no detector here. Detectors live in subpackages of their own (`aicordon.picket`, `aicordon.intent`) and present
4
+ one and the same `core.engine` contract; the core does not know them and must not — which is
5
+ exactly why the command layer of the products is shared rather than "similar".
6
+ """
aicordon/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Python3 -m aicordon — the same thing as the `aicordon` executable."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from .cli.umbrella import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
File without changes
aicordon/cli/bench.py ADDED
@@ -0,0 +1,341 @@
1
+ """Measuring the detector on YOUR documents: the numbers and the chart, without dependencies.
2
+
3
+ Why it is a command and not a page in the README. Every figure we publish was measured on our
4
+ machine and our corpora, and both differ from yours: the constant scales with the CPU, and how much
5
+ a document costs depends on what is written in it. A number you can reproduce where it will run is
6
+ worth more than a number you have to believe, so the tool carries the measurement with it.
7
+
8
+ What it reports is what we ourselves look at:
9
+
10
+ per document median, P10-P90, P99, max — the spread matters more than the average
11
+ per 1000 chars slope and intercept of the cost model, each with an interval
12
+ throughput documents and characters per second, and the whole run
13
+ flagged share how often it fires HERE — not an error rate, see below
14
+
15
+ The intervals are bootstrapped over documents rather than taken from a formula: the spread grows
16
+ with document size, so the constant-variance assumption behind the textbook interval is false here.
17
+ Two thousand resamples, a fixed seed, plain `random` — nothing to install.
18
+
19
+ The chart is written as SVG, which is text: no plotting library, no fonts to embed, and it opens in
20
+ any browser. That is not asceticism — a detector that must be installable inside somebody else's
21
+ dependency tree cannot bring matplotlib along for a chart.
22
+
23
+ **The flagged share is not a false-positive rate.** This code has no idea whether your documents
24
+ contain injections. It reports how often the detector fired here; calling that an error rate would
25
+ require labels, and labels are exactly what a corpus of your own documents does not come with.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import math
30
+ import random
31
+ import sys
32
+ import time
33
+ from typing import Iterable
34
+
35
+ BOOT = 2000
36
+ SEED = 20260801
37
+
38
+
39
+ def measure(det, docs: Iterable, repeat: int = 1, progress=None) -> list[dict]:
40
+ """One row per document: its size, the median of `repeat` timings, and whether it fired.
41
+
42
+ The median of repeats and not the first timing: the first touch of a document pays for the file
43
+ cache and for whatever else the machine was doing, and on short documents that noise is larger
44
+ than the thing being measured.
45
+ """
46
+ rows: list[dict] = []
47
+ for i, doc in enumerate(docs, 1):
48
+ ts = []
49
+ flagged = False
50
+ for _ in range(max(1, repeat)):
51
+ t = time.perf_counter()
52
+ for rep in det.reports([doc]):
53
+ flagged = rep.flagged
54
+ ts.append((time.perf_counter() - t) * 1000)
55
+ ts.sort()
56
+ rows.append({"id": doc.id, "size": len(doc.text), "ms": ts[len(ts) // 2],
57
+ "flagged": flagged})
58
+ if progress:
59
+ progress(i)
60
+ return rows
61
+
62
+
63
+ class Progress:
64
+ """A progress line for a run that takes minutes. Standard library, no dependency.
65
+
66
+ Two shapes for two readers. On a terminal it is ONE line, rewritten in place with `\r`, with a
67
+ bar, a rate and a forecast — a person watching wants to know whether to wait or to leave. Piped
68
+ into a file or a CI log there is no cursor to move, so it degrades to a line every so often;
69
+ a bar written with `\r` into a log file produces a single unreadable line kilometres long.
70
+
71
+ Always on stderr: the report goes to stdout and has to stay pipeable.
72
+
73
+ The forecast is deliberately naive — elapsed per document times what is left. The alternative,
74
+ a moving average, is worse here: document costs differ by an order of magnitude, and a rate
75
+ computed over the last few makes the estimate jump.
76
+ """
77
+
78
+ WIDTH = 28
79
+
80
+ def __init__(self, total: int | None = None, every: int = 50) -> None:
81
+ self.total = total if total and total > 0 else None
82
+ self.every = max(1, every)
83
+ self.tty = bool(getattr(sys.stderr, "isatty", lambda: False)())
84
+ self.t0 = time.perf_counter()
85
+ self.n = 0
86
+
87
+ def tick(self, n: int) -> None:
88
+ self.n = n
89
+ if not self.tty:
90
+ if n % max(self.every, 50) == 0:
91
+ print(f" {n}{'/' + str(self.total) if self.total else ''}…",
92
+ file=sys.stderr, flush=True)
93
+ return
94
+ if n % self.every and n != self.total:
95
+ return
96
+ done = time.perf_counter() - self.t0
97
+ rate = n / done if done else 0
98
+ if self.total:
99
+ filled = int(self.WIDTH * n / self.total)
100
+ bar = "█" * filled + "·" * (self.WIDTH - filled)
101
+ left = (self.total - n) / rate if rate else 0
102
+ tail = f" {n}/{self.total} {rate:.0f}/s {_clock(left)} left"
103
+ else:
104
+ bar = "·" * self.WIDTH
105
+ tail = f" {n} {rate:.0f}/s {_clock(done)} elapsed"
106
+ print(f"\r {bar}{tail} ", end="", file=sys.stderr, flush=True)
107
+
108
+ def done(self) -> None:
109
+ """Clears the line so the report starts on a clean one."""
110
+ if self.tty:
111
+ print("\r" + " " * (self.WIDTH + 44) + "\r", end="", file=sys.stderr, flush=True)
112
+
113
+
114
+ def _clock(seconds: float) -> str:
115
+ seconds = max(0, int(seconds))
116
+ if seconds < 60:
117
+ return f"{seconds}s"
118
+ if seconds < 3600:
119
+ return f"{seconds // 60}m{seconds % 60:02d}s"
120
+ return f"{seconds // 3600}h{(seconds % 3600) // 60:02d}m"
121
+
122
+
123
+ def _quantile(sorted_vals: list[float], p: float) -> float:
124
+ if not sorted_vals:
125
+ return 0.0
126
+ return sorted_vals[min(len(sorted_vals) - 1, int(len(sorted_vals) * p))]
127
+
128
+
129
+ def _fit(xs: list[float], ys: list[float]) -> tuple[float, float]:
130
+ """Least squares with an intercept, written out because there is no numpy here."""
131
+ n = len(xs)
132
+ if n < 2:
133
+ return (ys[0] if ys else 0.0), 0.0
134
+ mx = sum(xs) / n
135
+ my = sum(ys) / n
136
+ sxx = sum((x - mx) ** 2 for x in xs)
137
+ if sxx == 0:
138
+ return my, 0.0
139
+ slope = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / sxx
140
+ return my - slope * mx, slope
141
+
142
+
143
+ def _boot_fit(xs: list[float], ys: list[float], n: int = BOOT) -> dict:
144
+ """Intercept and slope, each with half the 95% interval — resampling documents, not residuals."""
145
+ a0, b0 = _fit(xs, ys)
146
+ rnd = random.Random(SEED)
147
+ N = len(xs)
148
+ if N < 8:
149
+ return {"intercept": a0, "intercept_pm": None, "slope": b0, "slope_pm": None}
150
+ A, B = [], []
151
+ for _ in range(n):
152
+ idx = [rnd.randrange(N) for _ in range(N)]
153
+ a, b = _fit([xs[i] for i in idx], [ys[i] for i in idx])
154
+ A.append(a)
155
+ B.append(b)
156
+ A.sort()
157
+ B.sort()
158
+
159
+ def pm(vals: list[float], point: float) -> float:
160
+ lo = vals[int(0.025 * len(vals))]
161
+ hi = vals[min(len(vals) - 1, int(0.975 * len(vals)))]
162
+ return max(point - lo, hi - point)
163
+
164
+ return {"intercept": a0, "intercept_pm": pm(A, a0), "slope": b0, "slope_pm": pm(B, b0)}
165
+
166
+
167
+ def summarize(rows: list[dict], load_ms: float, engine: str, version: str) -> dict:
168
+ """Every number the report carries. Sizes are in characters, times in milliseconds."""
169
+ times = sorted(r["ms"] for r in rows)
170
+ sizes = [float(r["size"]) for r in rows]
171
+ total_ms = sum(times)
172
+ total_chars = sum(sizes)
173
+ fit = _boot_fit(sizes, [r["ms"] for r in rows])
174
+ return {
175
+ "engine": engine,
176
+ "engine_version": version,
177
+ "documents": len(rows),
178
+ # NOT the startup of the command: this is the base coming up INSIDE an already running
179
+ # interpreter. The process as a whole costs several times more — interpreter, imports and
180
+ # this. Two different things under one word is how a README ends up arguing with its own
181
+ # example output, which is exactly what happened before this was renamed.
182
+ "base_load_ms": round(load_ms, 1),
183
+ "total_s": round(total_ms / 1000, 3),
184
+ "chars_total": int(total_chars),
185
+ "size": {
186
+ "median": int(_quantile(sorted(sizes), 0.5)),
187
+ "p10": int(_quantile(sorted(sizes), 0.10)),
188
+ "p90": int(_quantile(sorted(sizes), 0.90)),
189
+ "max": int(max(sizes)) if sizes else 0,
190
+ },
191
+ "ms_per_doc": {
192
+ "median": round(_quantile(times, 0.5), 3),
193
+ "p10": round(_quantile(times, 0.10), 3),
194
+ "p90": round(_quantile(times, 0.90), 3),
195
+ "p99": round(_quantile(times, 0.99), 3),
196
+ "max": round(times[-1], 3) if times else 0.0,
197
+ },
198
+ # The model everything else is derived from: cost = intercept + slope * characters.
199
+ "cost_model": {
200
+ "per_1000_chars_ms": round(fit["slope"] * 1000, 3),
201
+ "per_1000_chars_pm": None if fit["slope_pm"] is None else round(fit["slope_pm"] * 1000, 3),
202
+ "per_document_ms": round(fit["intercept"], 3),
203
+ "per_document_pm": None if fit["intercept_pm"] is None else round(fit["intercept_pm"], 3),
204
+ "confidence": "95%",
205
+ "method": "bootstrap over documents, 2000 resamples, fixed seed",
206
+ },
207
+ "throughput": {
208
+ "documents_per_s": round(len(rows) / (total_ms / 1000), 1) if total_ms else None,
209
+ "chars_per_s": int(total_chars / (total_ms / 1000)) if total_ms else None,
210
+ },
211
+ # The documents that cost the most PER CHARACTER, not the longest ones. This is a diagnostic
212
+ # and it has earned its place: a rule that hits one slot thousands of times in a signature
213
+ # line once cost 688 ms per 1000 characters where the rest of the corpus cost 2, and the
214
+ # only way that was ever going to be noticed is a list like this one.
215
+ "slowest_per_char": [
216
+ {"id": r["id"], "size": r["size"], "ms": round(r["ms"], 2),
217
+ "ms_per_1000": round(r["ms"] / max(1, r["size"]) * 1000, 2)}
218
+ for r in sorted(rows, key=lambda r: -(r["ms"] / max(1, r["size"])))[:5]
219
+ ],
220
+ # NOT an error rate: this code does not know what is in your documents. See the module note.
221
+ "flagged": {
222
+ "documents": sum(1 for r in rows if r["flagged"]),
223
+ "share": round(sum(1 for r in rows if r["flagged"]) / len(rows), 6) if rows else 0.0,
224
+ "note": "share of documents that fired here; not a false-positive rate — "
225
+ "these documents carry no labels",
226
+ },
227
+ }
228
+
229
+
230
+ def lines(s: dict) -> list[str]:
231
+ """The same numbers for a terminal, in the order they answer questions."""
232
+ cm = s["cost_model"]
233
+ fitted = cm["per_1000_chars_pm"] is not None
234
+ d = s["ms_per_doc"]
235
+ out = [
236
+ f" documents {s['documents']}, median length {s['size']['median']} characters "
237
+ f"(P10-P90 {s['size']['p10']}-{s['size']['p90']})",
238
+ f" per document median {d['median']} ms P10-P90 {d['p10']}-{d['p90']} "
239
+ f"P99 {d['p99']} max {d['max']}",
240
+ # The model is printed only when it was actually fitted. On a handful of documents the
241
+ # numbers still come out, and printing them with a "(95%)" beside them would dress a guess
242
+ # up as a measurement — the one thing this command exists not to do.
243
+ (f" cost model {cm['per_1000_chars_ms']} ± {cm['per_1000_chars_pm']} ms per 1000 "
244
+ f"characters, plus {cm['per_document_ms']} ± {cm['per_document_pm']} ms per document "
245
+ f"({cm['confidence']})") if fitted else
246
+ f" cost model not fitted: {s['documents']} document(s) is too few to separate the "
247
+ f"per-character cost from the per-document one",
248
+ f" throughput {s['throughput']['documents_per_s']} documents/s, "
249
+ f"{s['throughput']['chars_per_s']} characters/s",
250
+ f" base load {s['base_load_ms']} ms, once per process "
251
+ f"(the whole command costs more: interpreter and imports on top)",
252
+ f" whole run {s['total_s']} s",
253
+ f" dearest doc {s['slowest_per_char'][0]['ms_per_1000']} ms per 1000 characters "
254
+ f"({s['slowest_per_char'][0]['size']} chars) — against a median of "
255
+ f"{s['ms_per_doc']['median']} ms per document" if s.get("slowest_per_char") else "",
256
+ f" fired on {s['flagged']['documents']} of {s['documents']} documents "
257
+ f"({s['flagged']['share']:.2%}) — not an error rate, these documents carry no labels",
258
+ ]
259
+ return [x for x in out if x]
260
+
261
+
262
+ # --- the chart -----------------------------------------------------------------------------------
263
+ #
264
+ # Hand-written SVG, and it has to stay hand-written: the package declares no dependencies, and a
265
+ # chart is not a reason to break that. Everything here is arithmetic and string formatting.
266
+
267
+ W, H, PAD_L, PAD_B, PAD_T, PAD_R = 760, 420, 64, 48, 56, 20
268
+
269
+
270
+ def _nice(v: float) -> float:
271
+ """A round number at or above `v`, for an axis a human can read."""
272
+ if v <= 0:
273
+ return 1.0
274
+ exp = math.floor(math.log10(v))
275
+ base = 10 ** exp
276
+ for m in (1, 2, 2.5, 5, 10):
277
+ if m * base >= v:
278
+ return m * base
279
+ return 10 * base
280
+
281
+
282
+ def svg(rows: list[dict], s: dict, title: str = "Time to check one document") -> str:
283
+ xs = [r["size"] for r in rows] or [1]
284
+ ys = [r["ms"] for r in rows] or [0.0]
285
+ xmax = _nice(max(xs))
286
+ ymax = _nice(max(ys))
287
+ plot_w = W - PAD_L - PAD_R
288
+ plot_h = H - PAD_T - PAD_B
289
+
290
+ def px(x: float) -> float:
291
+ return PAD_L + plot_w * (x / xmax)
292
+
293
+ def py(y: float) -> float:
294
+ return PAD_T + plot_h * (1 - y / ymax)
295
+
296
+ cm = s["cost_model"]
297
+ a = cm["per_document_ms"]
298
+ b = cm["per_1000_chars_ms"] / 1000.0
299
+
300
+ dots = "".join(
301
+ f'<circle cx="{px(r["size"]):.1f}" cy="{py(r["ms"]):.1f}" r="2.6" '
302
+ f'class="{"hit" if r["flagged"] else "dot"}"/>'
303
+ for r in rows)
304
+ grid = "".join(
305
+ f'<line x1="{PAD_L}" y1="{py(ymax * k / 4):.1f}" x2="{W - PAD_R}" y2="{py(ymax * k / 4):.1f}"'
306
+ f' class="grid"/><text x="{PAD_L - 8}" y="{py(ymax * k / 4) + 4:.1f}" class="tick" '
307
+ f'text-anchor="end">{ymax * k / 4:g}</text>'
308
+ for k in range(5))
309
+ xticks = "".join(
310
+ f'<text x="{px(xmax * k / 4):.1f}" y="{H - PAD_B + 20}" class="tick" '
311
+ f'text-anchor="middle">{int(xmax * k / 4)}</text>'
312
+ for k in range(5))
313
+ pm = f" ± {cm['per_1000_chars_pm']}" if cm["per_1000_chars_pm"] is not None else ""
314
+
315
+ return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" \
316
+ height="{H}" font-family="system-ui, sans-serif">
317
+ <style>
318
+ .bg {{ fill: #fbfbf9; }} .ink {{ fill: #1a1a19; }} .muted {{ fill: #6b6a66; }}
319
+ .grid {{ stroke: #e6e4de; stroke-width: 1; }} .tick {{ fill: #8a8880; font-size: 11px; }}
320
+ .dot {{ fill: #3f7fd0; fill-opacity: .45; }} .hit {{ fill: #d9663f; fill-opacity: .55; }}
321
+ .fit {{ stroke: #6b6a66; stroke-width: 1.6; stroke-dasharray: 6 4; fill: none; }}
322
+ @media (prefers-color-scheme: dark) {{
323
+ .bg {{ fill: #1a1a19; }} .ink {{ fill: #ffffff; }} .muted {{ fill: #898781; }}
324
+ .grid {{ stroke: #333230; }} .tick {{ fill: #6b6a66; }}
325
+ }}
326
+ </style>
327
+ <rect class="bg" width="{W}" height="{H}"/>
328
+ <text x="16" y="26" class="ink" font-size="15" font-weight="600">{title}</text>
329
+ <text x="16" y="44" class="muted" font-size="11.5">{s['documents']} documents · \
330
+ {cm['per_1000_chars_ms']}{pm} ms per 1000 characters ({cm['confidence']}) · \
331
+ {s['engine']} {s['engine_version']}</text>
332
+ {grid}{xticks}
333
+ <line class="fit" x1="{px(0):.1f}" y1="{py(a):.1f}" x2="{px(xmax):.1f}" \
334
+ y2="{py(min(ymax, a + b * xmax)):.1f}"/>
335
+ {dots}
336
+ <text x="{PAD_L + plot_w / 2:.0f}" y="{H - 8}" class="muted" font-size="11" \
337
+ text-anchor="middle">document size, characters</text>
338
+ <text x="14" y="{PAD_T + plot_h / 2:.0f}" class="muted" font-size="11" \
339
+ transform="rotate(-90 14 {PAD_T + plot_h / 2:.0f})" text-anchor="middle">milliseconds</text>
340
+ </svg>
341
+ """