inferencekit-cli 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.
Files changed (104) hide show
  1. inferencekit/__init__.py +9 -0
  2. inferencekit/__main__.py +5 -0
  3. inferencekit/analysis/__init__.py +1 -0
  4. inferencekit/analysis/ncu_csv.py +284 -0
  5. inferencekit/analysis/nsys_sqlite.py +518 -0
  6. inferencekit/canonical.py +104 -0
  7. inferencekit/checkpoint.py +396 -0
  8. inferencekit/cli.py +274 -0
  9. inferencekit/cuda/bw_peak.cu +252 -0
  10. inferencekit/cuda/torch_bw.py +60 -0
  11. inferencekit/engines/__init__.py +7 -0
  12. inferencekit/engines/adapters.py +190 -0
  13. inferencekit/engines/clip/__init__.py +13 -0
  14. inferencekit/engines/clip/comfyui.py +749 -0
  15. inferencekit/engines/clip/diffusers_ltx.py +601 -0
  16. inferencekit/engines/clip/noise.py +46 -0
  17. inferencekit/engines/clip/official_ltx.py +316 -0
  18. inferencekit/engines/clip/server.py +542 -0
  19. inferencekit/engines/clip/stepdump.py +182 -0
  20. inferencekit/engines/gates.py +94 -0
  21. inferencekit/engines/sglang_worker.py +86 -0
  22. inferencekit/engines/vllm_worker.py +129 -0
  23. inferencekit/evidence.py +427 -0
  24. inferencekit/gpuinfo.py +93 -0
  25. inferencekit/hardware.py +135 -0
  26. inferencekit/mcp_server.py +178 -0
  27. inferencekit/ops/__init__.py +30 -0
  28. inferencekit/ops/bench.py +184 -0
  29. inferencekit/ops/bench_audio.py +384 -0
  30. inferencekit/ops/bench_clip.py +819 -0
  31. inferencekit/ops/bench_image.py +393 -0
  32. inferencekit/ops/bench_rounds.py +297 -0
  33. inferencekit/ops/bench_serve.py +866 -0
  34. inferencekit/ops/ceiling.py +135 -0
  35. inferencekit/ops/compare.py +378 -0
  36. inferencekit/ops/correctness.py +153 -0
  37. inferencekit/ops/doctor.py +486 -0
  38. inferencekit/ops/gates.py +1772 -0
  39. inferencekit/ops/gates_clip.py +930 -0
  40. inferencekit/ops/intake.py +1321 -0
  41. inferencekit/ops/ledger.py +749 -0
  42. inferencekit/ops/manifest.py +309 -0
  43. inferencekit/ops/ncu.py +183 -0
  44. inferencekit/ops/nsys.py +224 -0
  45. inferencekit/ops/prompts.py +316 -0
  46. inferencekit/ops/quality.py +2161 -0
  47. inferencekit/ops/quality_clip.py +690 -0
  48. inferencekit/ops/quality_clip_steps.py +341 -0
  49. inferencekit/ops/report.py +317 -0
  50. inferencekit/ops/review.py +119 -0
  51. inferencekit/ops/rocprof.py +89 -0
  52. inferencekit/ops/roofline.py +392 -0
  53. inferencekit/ops/store.py +61 -0
  54. inferencekit/proc.py +172 -0
  55. inferencekit/prompts/ATTRIBUTION.md +56 -0
  56. inferencekit/prompts/__init__.py +167 -0
  57. inferencekit/prompts/build.py +518 -0
  58. inferencekit/prompts/dev-load.jsonl.gz +0 -0
  59. inferencekit/prompts/dev-load.manifest.json +212 -0
  60. inferencekit/prompts/dev.jsonl.gz +0 -0
  61. inferencekit/prompts/dev.manifest.json +214 -0
  62. inferencekit/prompts/exclusions.json +54 -0
  63. inferencekit/quality/__init__.py +23 -0
  64. inferencekit/quality/client.py +66 -0
  65. inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
  66. inferencekit/quality/fake.py +329 -0
  67. inferencekit/quality/gsm8k.py +224 -0
  68. inferencekit/quality/logprobs.py +106 -0
  69. inferencekit/quality/metrics.py +235 -0
  70. inferencekit/quality/niah.py +63 -0
  71. inferencekit/quality/parquet.py +296 -0
  72. inferencekit/quality/sampling.py +262 -0
  73. inferencekit/quality/step_replay.py +217 -0
  74. inferencekit/quality/video_metrics.py +304 -0
  75. inferencekit/registry.py +206 -0
  76. inferencekit/review/__init__.py +6 -0
  77. inferencekit/review/diff.py +163 -0
  78. inferencekit/review/media.py +181 -0
  79. inferencekit/review/page.py +920 -0
  80. inferencekit/review/rundir.py +844 -0
  81. inferencekit/roofline_headers.py +504 -0
  82. inferencekit/schemas/evidence.v1.json +130 -0
  83. inferencekit/schemas/evidence.v2.json +499 -0
  84. inferencekit/schemas/intake.v1.json +276 -0
  85. inferencekit/schemas/run-manifest.v1.json +118 -0
  86. inferencekit/serve/__init__.py +8 -0
  87. inferencekit/serve/audio_client.py +330 -0
  88. inferencekit/serve/client.py +446 -0
  89. inferencekit/serve/clip_client.py +351 -0
  90. inferencekit/serve/clip_rounds.py +429 -0
  91. inferencekit/serve/image_client.py +373 -0
  92. inferencekit/serve/loadgen.py +136 -0
  93. inferencekit/serve/metrics.py +203 -0
  94. inferencekit/serve/profiles.py +105 -0
  95. inferencekit/serve/prompts_io.py +125 -0
  96. inferencekit/serve/rounds.py +1111 -0
  97. inferencekit/serve/servers.py +897 -0
  98. inferencekit/stats.py +249 -0
  99. inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
  100. inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
  101. inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
  102. inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
  103. inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  104. inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
@@ -0,0 +1,518 @@
1
+ """Build the prompt sets from openly licensed sources, deterministically.
2
+
3
+ Sources (downloaded once into a cache directory; their sha256 is recorded in every
4
+ manifest, so a rebuild from different bytes is visible):
5
+
6
+ * databricks-dolly-15k (CC BY-SA 3.0): instructions without context feed very_short
7
+ and short; context + instruction pairs feed medium.
8
+ * GSM8K train split (MIT): grade-school maths questions feed short.
9
+ * Public-domain novels (Project Gutenberg plain-text editions, licence boilerplate
10
+ stripped): passages of ~1,700-2,600 words plus a question feed long.
11
+
12
+ Groups and the dev / sealed split: every candidate belongs to a *group*, the unit that is
13
+ split and deduplicated. A dolly row with a context is grouped by the sha256 of its
14
+ normalised context paragraph (many rows ask different questions about one paragraph), a
15
+ row without context by its normalised instruction, a GSM8K question by its normalised
16
+ text, and a novel passage by its book. Each group yields at most one candidate (novels:
17
+ one per passage), and a group is assigned to ``dev`` or ``sealed`` by one bit of
18
+ ``sha256(seed | split | group)``, so rows that share a context paragraph always land on
19
+ the same side, and no two prompts anywhere share a context (no shared prefix for a
20
+ prefix cache to reuse). As a second guard, a sealed candidate is dropped when more than
21
+ half of its 12-word shingles appear in the dev sets, and any candidate is dropped when
22
+ more than half of its shingles appear in an item already chosen for the same split.
23
+ Inside a split, groups are ranked by another seeded hash; the single-user set takes the
24
+ first 16 per bucket and the load set the next ones, so the four sets are disjoint.
25
+
26
+ The code and the seed are public, so a sealed set built from the public seed alone could
27
+ be rebuilt by anyone, including an agent being evaluated. ``prompts seal`` therefore
28
+ takes a secret: the sealed sets are drawn from every group the dev sets do not use and
29
+ ranked by ``sha256(secret | ...)``. The manifests commit to the secret by
30
+ ``secret_sha256`` = the sha256 of the secret file's bytes as ``prompts seal`` writes them
31
+ (64 hex characters and a newline), i.e. ``sha256sum SEED.secret``. Revealing the file
32
+ after the evaluation lets anyone check that hash, rebuild the sealed set
33
+ (``prompts verify --secret-file``) and compare it with the pre-registered sha256.
34
+
35
+ Exclusions: text the release leak scanner would flag (names that must never appear in a
36
+ tracked file) is excluded. The scanner's rules are private and never in the repository;
37
+ they are read at build time only from a file given explicitly, with ``--blocklist FILE`` or
38
+ ``$INFERENCEKIT_PROMPT_BLOCKLIST`` (scanner syntax). The source rows they
39
+ exclude are recorded, by position only, in ``exclusions.json`` next to the shipped sets
40
+ (keyed by the source file's sha256), so a public rebuild without the private rules
41
+ excludes the same rows and gives byte-identical sets.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import hashlib
47
+ import json
48
+ import os
49
+ import re
50
+ import urllib.request
51
+ from pathlib import Path
52
+
53
+ from inferencekit.prompts import BUCKET_RULES, BUCKETS, PROXY_FACTOR, gzip_bytes, in_bucket, proxy_tokens, words
54
+
55
+ SEED = "inferencekit-prompts-v1"
56
+ VERSION = "1.0"
57
+ MAX_TOKENS = 256
58
+ PER_BUCKET = 16
59
+ LOAD_TARGET = 1280 # >= 1,200 unique short/medium prompts per load set
60
+ LOAD_MEDIUM_MAX = 280
61
+
62
+ DOLLY = {"name": "databricks-dolly-15k", "license": "CC-BY-SA-3.0", "file": "dolly.jsonl",
63
+ "url": "https://huggingface.co/datasets/databricks/databricks-dolly-15k/resolve/"
64
+ "bdd27f4d94b9c1f951818a7da7fd7aeea5dbff1a/databricks-dolly-15k.jsonl"}
65
+ GSM8K = {"name": "gsm8k-train", "license": "MIT", "file": "gsm8k_train.jsonl",
66
+ "url": "https://raw.githubusercontent.com/openai/grade-school-math/"
67
+ "3101c7d5072418e28b9008a6636bde82a006892c/grade_school_math/data/train.jsonl"}
68
+ # Public-domain novels (US). Gutenberg ebook numbers -> title, for attribution.
69
+ BOOKS = {
70
+ 11: "Alice's Adventures in Wonderland (Lewis Carroll)", 35: "The Time Machine (H. G. Wells)",
71
+ 36: "The War of the Worlds (H. G. Wells)", 43: "The Strange Case of Dr Jekyll and Mr Hyde (R. L. Stevenson)",
72
+ 76: "Adventures of Huckleberry Finn (Mark Twain)", 84: "Frankenstein (Mary Shelley)",
73
+ 98: "A Tale of Two Cities (Charles Dickens)", 120: "Treasure Island (R. L. Stevenson)",
74
+ 158: "Emma (Jane Austen)", 161: "Sense and Sensibility (Jane Austen)",
75
+ 174: "The Picture of Dorian Gray (Oscar Wilde)", 215: "The Call of the Wild (Jack London)",
76
+ 219: "Heart of Darkness (Joseph Conrad)", 345: "Dracula (Bram Stoker)",
77
+ 768: "Wuthering Heights (Emily Bronte)", 1260: "Jane Eyre (Charlotte Bronte)",
78
+ 1342: "Pride and Prejudice (Jane Austen)", 1400: "Great Expectations (Charles Dickens)",
79
+ 1661: "The Adventures of Sherlock Holmes (Arthur Conan Doyle)",
80
+ 2554: "Crime and Punishment (Fyodor Dostoevsky, tr. Constance Garnett)",
81
+ 2701: "Moby Dick (Herman Melville)",
82
+ }
83
+ GUTENBERG_URL = "https://www.gutenberg.org/cache/epub/{n}/pg{n}.txt"
84
+ GUTENBERG_LICENSE = "public-domain"
85
+ LONG_QUESTIONS = (
86
+ "Summarize the passage above in about 150 words.",
87
+ "List the characters who appear in the passage above and describe what each of them does.",
88
+ "What is the central conflict or tension in the passage above? Support your answer with two details from the text.",
89
+ "Describe the setting of the passage above and how it shapes the mood.",
90
+ "Write five questions a teacher could ask about the passage above, each with a one-sentence answer.",
91
+ "Explain what happens in the passage above, step by step, in the order the events occur.",
92
+ )
93
+ PASSAGE_FRACTIONS = (0.1, 0.25, 0.4, 0.55, 0.7, 0.85) # >= 3,000 words apart in every book used
94
+ EXCLUSIONS_FILE = "exclusions.json"
95
+ SHINGLE = 12 # words per shingle for the near-duplicate guard
96
+ MAX_SHARED = 0.5 # largest share of a candidate's shingles allowed to appear in the other split / same set
97
+
98
+
99
+ # --------------------------------------------------------------------------- sources
100
+
101
+ def source_files(cache: Path) -> dict[str, Path]:
102
+ files = {"dolly": cache / DOLLY["file"], "gsm8k": cache / GSM8K["file"]}
103
+ for n in BOOKS:
104
+ files[f"gutenberg:{n}"] = cache / "gutenberg" / f"pg{n}.txt"
105
+ return files
106
+
107
+
108
+ def fetch(cache: Path, log=print) -> dict[str, Path]:
109
+ """Download any missing source file into ``cache``. Returns the source map."""
110
+ cache.mkdir(parents=True, exist_ok=True)
111
+ (cache / "gutenberg").mkdir(exist_ok=True)
112
+ urls = {"dolly": DOLLY["url"], "gsm8k": GSM8K["url"]}
113
+ urls.update({f"gutenberg:{n}": GUTENBERG_URL.format(n=n) for n in BOOKS})
114
+ files = source_files(cache)
115
+ for key, path in files.items():
116
+ if path.is_file() and path.stat().st_size > 0:
117
+ continue
118
+ log(f"downloading {urls[key]}")
119
+ req = urllib.request.Request(urls[key], headers={"User-Agent": "inferencekit-prompt-builder"})
120
+ with urllib.request.urlopen(req, timeout=120) as r: # noqa: S310 - fixed https URLs above
121
+ data = r.read()
122
+ tmp = path.with_suffix(path.suffix + ".part")
123
+ tmp.write_bytes(data)
124
+ tmp.replace(path)
125
+ return files
126
+
127
+
128
+ def source_table(cache: Path) -> list[dict]:
129
+ files = source_files(cache)
130
+ rows = [{"name": DOLLY["name"], "license": DOLLY["license"], "url": DOLLY["url"],
131
+ "sha256": sha256_file(files["dolly"])},
132
+ {"name": GSM8K["name"], "license": GSM8K["license"], "url": GSM8K["url"],
133
+ "sha256": sha256_file(files["gsm8k"])}]
134
+ for n, title in BOOKS.items():
135
+ rows.append({"name": f"gutenberg-{n}", "title": title, "license": GUTENBERG_LICENSE,
136
+ "url": GUTENBERG_URL.format(n=n), "sha256": sha256_file(files[f"gutenberg:{n}"])})
137
+ return rows
138
+
139
+
140
+ # --------------------------------------------------------------------------- hashing
141
+
142
+ def _h(*parts) -> str:
143
+ return hashlib.sha256("|".join(str(p) for p in parts).encode("utf-8")).hexdigest()
144
+
145
+
146
+ def split_of(key: str, seed: str = SEED) -> str:
147
+ """'dev' or 'sealed' from one bit of a seeded hash of the source item's key."""
148
+ return "dev" if int(_h(seed, "split", key)[:2], 16) % 2 == 0 else "sealed"
149
+
150
+
151
+ def _norm(text: str) -> str:
152
+ return " ".join(text.lower().split())
153
+
154
+
155
+ def blocklist_path(explicit: str | None = None) -> Path | None:
156
+ """The private scrub-patterns file, only when one is named: ``explicit`` (the ``--blocklist``
157
+ flag), else ``$INFERENCEKIT_PROMPT_BLOCKLIST``. Nothing is read by default; ``none`` or an
158
+ empty value means no file. A named file that does not exist is an error."""
159
+ value = explicit if explicit is not None else os.environ.get("INFERENCEKIT_PROMPT_BLOCKLIST")
160
+ if value is None or not value.strip() or value.strip().lower() == "none":
161
+ return None
162
+ p = Path(value.strip()).expanduser()
163
+ if not p.is_file():
164
+ raise FileNotFoundError(f"blocklist file not found: {p}")
165
+ return p
166
+
167
+
168
+ def load_rules(path: Path | None) -> list[re.Pattern]:
169
+ """Byte regexes from a scrub-patterns file (the release scanner's syntax: ``literal:TEXT``,
170
+ ``regex:PATTERN``, ``TEXT``, each optionally ``==>REPLACEMENT``; ``#scan RULE`` is a rule too;
171
+ other ``#`` lines are comments or directives)."""
172
+ if path is None:
173
+ return []
174
+ rules = []
175
+ for line in Path(path).read_text(encoding="utf-8").splitlines():
176
+ raw = line.rstrip("\r\n")
177
+ if not raw.strip() or raw.startswith("#@"):
178
+ continue
179
+ if raw.startswith("#scan "):
180
+ raw = raw[len("#scan "):]
181
+ elif raw.startswith("#"):
182
+ continue
183
+ match = raw.rsplit("==>", 1)[0] if "==>" in raw else raw
184
+ if match.startswith("regex:"):
185
+ rules.append(re.compile(match[len("regex:"):].encode("utf-8")))
186
+ else:
187
+ text = match[len("literal:"):] if match.startswith("literal:") else match
188
+ if text:
189
+ rules.append(re.compile(re.escape(text.encode("utf-8"))))
190
+ return rules
191
+
192
+
193
+ def blocked(text: str, rules: list[re.Pattern]) -> bool:
194
+ """True if the scanner would flag the text (as written, or as a JSONL string)."""
195
+ if not rules:
196
+ return False
197
+ forms = (text.encode("utf-8"), json.dumps(text, ensure_ascii=False).encode("utf-8"))
198
+ return any(r.search(f) for r in rules for f in forms)
199
+
200
+
201
+ def read_exclusions(path: Path) -> dict[str, set[str]]:
202
+ """{source sha256: {row refs}} from an exclusions file (empty if there is none)."""
203
+ try:
204
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
205
+ except (OSError, ValueError):
206
+ return {}
207
+ return {sha: set(refs) for sha, refs in (data.get("excluded") or {}).items()}
208
+
209
+
210
+ def write_exclusions(path: Path, excluded: dict[str, set[str]], names: dict[str, str]) -> None:
211
+ data = {"format": 1,
212
+ "rule": "source rows the release leak scanner's private rules would flag; listed by position in the "
213
+ "source file (keyed by that file's sha256) so a rebuild without the private rules matches",
214
+ "sources": {sha: names.get(sha, "?") for sha in sorted(excluded)},
215
+ "excluded": {sha: sorted(refs, key=_ref_order) for sha, refs in sorted(excluded.items()) if refs}}
216
+ Path(path).write_text(json.dumps(data, indent=1) + "\n", encoding="utf-8")
217
+
218
+
219
+ def _ref_order(ref: str):
220
+ return tuple(int(x) if x.isdigit() else x for x in re.split(r"[:]", ref.replace("row", "")))
221
+
222
+
223
+ def exclusions_digest(excluded: dict[str, set[str]]) -> str:
224
+ """sha256 over the sorted (source sha256, ref) pairs actually applied: recorded in each manifest."""
225
+ lines = sorted(f"{sha} {ref}" for sha, refs in excluded.items() for ref in refs)
226
+ return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
227
+
228
+
229
+ def shingles(text: str, n: int = SHINGLE) -> set[str]:
230
+ w = text.lower().split()
231
+ return {" ".join(w[i:i + n]) for i in range(len(w) - n + 1)}
232
+
233
+
234
+ def shared_fraction(sh: set[str], seen: set[str]) -> float:
235
+ return len(sh & seen) / len(sh) if sh else 0.0
236
+
237
+
238
+ # --------------------------------------------------------------------------- candidates
239
+
240
+ def _dolly_candidates(path: Path) -> list[dict]:
241
+ """One candidate per group: a context paragraph (rows asking about the same paragraph are one
242
+ group) or, without context, a normalised instruction. The first row in file order that fits a
243
+ bucket represents its group."""
244
+ out, seen = [], set()
245
+ for n, line in enumerate(path.read_text(encoding="utf-8").splitlines()):
246
+ if not line.strip():
247
+ continue
248
+ o = json.loads(line)
249
+ ins, ctx = " ".join(o["instruction"].split()), o.get("context", "").strip()
250
+ if not ins:
251
+ continue
252
+ if ctx:
253
+ ctx = "\n\n".join(" ".join(p.split()) for p in re.split(r"\n\s*\n", ctx) if p.strip())
254
+ text, bucket = f"{ctx}\n\n{ins}", "medium"
255
+ key = "dolly-context:" + hashlib.sha256(_norm(ctx).encode("utf-8")).hexdigest()
256
+ if not in_bucket(text, "medium"):
257
+ continue
258
+ else:
259
+ text = ins
260
+ key = "dolly:" + _norm(text)
261
+ if in_bucket(text, "very_short") and text.endswith("?"):
262
+ bucket = "very_short"
263
+ elif in_bucket(text, "short"):
264
+ bucket = "short"
265
+ else:
266
+ continue
267
+ if key in seen:
268
+ continue
269
+ seen.add(key)
270
+ out.append({"bucket": bucket, "text": text, "key": key, "ref": f"dolly:row{n + 1}", "src": "dolly",
271
+ "source": f"{DOLLY['name']}/{DOLLY['license']}", "source_ref": f"{DOLLY['name']}:line{n + 1}"})
272
+ return out
273
+
274
+
275
+ def _gsm8k_candidates(path: Path) -> list[dict]:
276
+ out, seen = [], set()
277
+ for n, line in enumerate(path.read_text(encoding="utf-8").splitlines()):
278
+ if not line.strip():
279
+ continue
280
+ q = " ".join(json.loads(line)["question"].split())
281
+ key = "gsm8k:" + _norm(q)
282
+ if key in seen or not in_bucket(q, "short"):
283
+ continue
284
+ seen.add(key)
285
+ out.append({"bucket": "short", "text": q, "key": key, "ref": f"gsm8k:row{n + 1}", "src": "gsm8k",
286
+ "source": f"{GSM8K['name']}/{GSM8K['license']}", "source_ref": f"{GSM8K['name']}:line{n + 1}"})
287
+ return out
288
+
289
+
290
+ def gutenberg_paragraphs(raw: str) -> list[str]:
291
+ """Body paragraphs with the distributor's licence header and footer removed."""
292
+ text = raw.replace("\r\n", "\n").lstrip("")
293
+ start = re.search(r"^\*\*\* ?START OF.*$", text, re.M)
294
+ end = re.search(r"^\*\*\* ?END OF.*$", text, re.M)
295
+ body = text[start.end() if start else 0: end.start() if end else len(text)]
296
+ paras = [" ".join(p.split()) for p in re.split(r"\n\s*\n", body) if p.strip()]
297
+ return [p for p in paras if "gutenberg" not in p.lower()]
298
+
299
+
300
+ def _long_candidates(files: dict[str, Path], seed: str) -> list[dict]:
301
+ out = []
302
+ for n in BOOKS:
303
+ paras = gutenberg_paragraphs(files[f"gutenberg:{n}"].read_text(encoding="utf-8", errors="replace"))
304
+ for k, frac in enumerate(PASSAGE_FRACTIONS):
305
+ target = 1700 + int(_h(seed, "len", n, k)[:4], 16) % 900 # 1,700-2,599 words
306
+ i = int(len(paras) * frac)
307
+ chunk, count = [], 0
308
+ while i < len(paras) and count < target:
309
+ chunk.append(paras[i])
310
+ count += words(paras[i])
311
+ i += 1
312
+ question = LONG_QUESTIONS[int(_h(seed, "q", n, k)[:4], 16) % len(LONG_QUESTIONS)]
313
+ passage = "\n\n".join(chunk)
314
+ text = ("Read the following passage from a public-domain novel and answer the question after it.\n\n"
315
+ f"{passage}\n\nQuestion: {question}")
316
+ if not in_bucket(text, "long"):
317
+ continue
318
+ out.append({"bucket": "long", "text": text, "key": f"gutenberg:{n}", "rank_key": f"gutenberg:{n}:{k}",
319
+ "ref": f"gutenberg:{n}:passage{k}", "src": f"gutenberg:{n}",
320
+ "order": k, "source": f"gutenberg-{n}/{GUTENBERG_LICENSE}",
321
+ "source_ref": f"{BOOKS[n]}, passage at {int(frac * 100)}% of the book"})
322
+ return out
323
+
324
+
325
+ def candidates(cache: Path, seed: str = SEED, exclusions: dict[str, set[str]] | None = None,
326
+ rules: list[re.Pattern] | None = None) -> tuple[list[dict], dict[str, set[str]]]:
327
+ """(candidates, exclusions applied). A row is excluded when an exclusions file lists it for this
328
+ exact source file (by sha256) or a private scanner rule matches its text."""
329
+ files = source_files(cache)
330
+ missing = [str(p) for p in files.values() if not p.is_file()]
331
+ if missing:
332
+ raise FileNotFoundError(f"missing source files (run with fetch): {missing[:3]}")
333
+ shas = {k: sha256_file(p) for k, p in files.items()}
334
+ cands = _dolly_candidates(files["dolly"]) + _gsm8k_candidates(files["gsm8k"]) + _long_candidates(files, seed)
335
+ listed = exclusions or {}
336
+ applied: dict[str, set[str]] = {}
337
+ kept = []
338
+ for c in cands:
339
+ sha = shas[c["src"]]
340
+ if c["ref"] in listed.get(sha, ()) or blocked(c["text"], rules or []):
341
+ applied.setdefault(sha, set()).add(c["ref"])
342
+ continue
343
+ c["split"] = split_of(c["key"], seed)
344
+ kept.append(c)
345
+ return kept, applied
346
+
347
+
348
+ def source_names(cache: Path) -> dict[str, str]:
349
+ """{sha256: source name} for the exclusions file."""
350
+ files = source_files(cache)
351
+ names = {"dolly": DOLLY["name"], "gsm8k": GSM8K["name"]}
352
+ return {sha256_file(p): names.get(k, k.replace(":", "-")) for k, p in files.items()}
353
+
354
+
355
+ # --------------------------------------------------------------------------- selection
356
+
357
+ def _item(c: dict, set_tag: str, i: int) -> dict:
358
+ return {"id": f"{c['bucket']}-{set_tag}{i:04d}", "bucket": c["bucket"],
359
+ "messages": [{"role": "user", "content": c["text"]}], "max_tokens": MAX_TOKENS,
360
+ "source": c["source"], "kind": "chat", "source_ref": c["source_ref"],
361
+ "proxy_tokens": proxy_tokens(c["text"])}
362
+
363
+
364
+ def _pick(cands: list[dict], split: str, seed: str, secret: str | None) -> tuple[dict, dict]:
365
+ """({bucket: single candidates}, {bucket: load candidates}) for one split."""
366
+ pools: dict[str, list[dict]] = {b: [] for b in BUCKETS}
367
+ avoid: set[str] = set() # shingles of every dev prompt (sealed only)
368
+ if split == "sealed":
369
+ # Never a group the dev sets use, and never text that mostly repeats a dev prompt.
370
+ dev_single, dev_load = _pick(cands, "dev", seed, None)
371
+ dev = [c for group in (dev_single, dev_load) for cs in group.values() for c in cs]
372
+ used = {c["key"] for c in dev}
373
+ for c in dev:
374
+ avoid |= shingles(c["text"])
375
+ for c in cands:
376
+ # With a secret: every group the dev sets do not use (long passages: every book they do
377
+ # not use), ranked by the secret, so nobody can predict the sealed set without it and
378
+ # anyone can rebuild it once it is revealed. Without one: the public sealed half.
379
+ if c["key"] not in used and (secret or c["split"] == "sealed"):
380
+ pools[c["bucket"]].append(c)
381
+ salt = secret or seed
382
+ else:
383
+ for c in cands:
384
+ if c["split"] == split:
385
+ pools[c["bucket"]].append(c)
386
+ salt = seed
387
+ seen: set[str] = set()
388
+ for b in BUCKETS:
389
+ # Long passages: one per book first (order k), then the next passage of each book.
390
+ pool = sorted(pools[b], key=lambda c: (c.get("order", 0), _h(salt, "rank", split, b,
391
+ c.get("rank_key", c["key"]))))
392
+ kept = []
393
+ for c in pool: # near-duplicate guard, in rank order
394
+ sh = shingles(c["text"])
395
+ if shared_fraction(sh, avoid) > MAX_SHARED or shared_fraction(sh, seen) > MAX_SHARED:
396
+ continue
397
+ seen |= sh
398
+ kept.append(c)
399
+ pools[b] = kept
400
+ single, rest = {}, {}
401
+ for b in BUCKETS:
402
+ if len(pools[b]) < PER_BUCKET:
403
+ raise ValueError(f"{split}: only {len(pools[b])} {b} candidates (need {PER_BUCKET})")
404
+ single[b] = pools[b][:PER_BUCKET]
405
+ rest[b] = pools[b][PER_BUCKET:]
406
+ n_med = min(LOAD_MEDIUM_MAX, len(rest["medium"]))
407
+ n_short = LOAD_TARGET - n_med
408
+ if len(rest["short"]) < n_short:
409
+ raise ValueError(f"{split}: only {len(rest['short'])} short load candidates (need {n_short})")
410
+ return single, {"short": rest["short"][:n_short], "medium": rest["medium"][:n_med]}
411
+
412
+
413
+ def select(cands: list[dict], split: str, seed: str = SEED, secret: str | None = None) -> dict[str, list[dict]]:
414
+ """{'single': [...], 'load': [...]} for one split. Disjoint by construction.
415
+
416
+ ``dev``: the dev half of the public hash split, ranked by the public seed.
417
+ ``sealed`` without a secret: the other half, ranked by the public seed (reproducible by
418
+ anyone, so only "sealed" against accidents). ``sealed`` with a secret: see ``_pick``.
419
+ """
420
+ tags = {"dev": ("", "load-"), "sealed": ("s", "sload-")}[split]
421
+ single, load = _pick(cands, split, seed, secret)
422
+ out_single = [_item(c, tags[0], i + 1) for b in BUCKETS for i, c in enumerate(single[b])]
423
+ out_load = [_item(c, tags[1], i + 1) for b in ("short", "medium") for i, c in enumerate(load[b])]
424
+ return {"single": out_single, "load": out_load}
425
+
426
+
427
+
428
+ def sha256_file(path: Path) -> str:
429
+ """sha256 of a file's raw bytes (source files, manifests)."""
430
+ return hashlib.sha256(Path(path).read_bytes()).hexdigest()
431
+
432
+
433
+ # --------------------------------------------------------------------------- writing
434
+
435
+ def jsonl_bytes(items: list[dict]) -> bytes:
436
+ return "".join(json.dumps(it, ensure_ascii=False) + "\n" for it in items).encode("utf-8")
437
+
438
+
439
+ SECRET_RULE = ("secret_sha256 = sha256 of the secret file's bytes as prompts seal writes them (64 hex characters and "
440
+ "a newline): `sha256sum SEED.secret`, or `echo <secret> | sha256sum`")
441
+
442
+
443
+ def manifest(name: str, items: list[dict], data: bytes, sealed: bool, role: str, sources: list[dict],
444
+ seed: str = SEED, secret: str | None = None, secret_sha256: str | None = None,
445
+ exclusions: dict | None = None) -> dict:
446
+ counts = {b: sum(1 for it in items if it["bucket"] == b) for b in BUCKETS}
447
+ return {"name": name, "version": VERSION, "n": len(items), "buckets": {b: n for b, n in counts.items() if n},
448
+ "sha256": hashlib.sha256(data).hexdigest(), "sealed": sealed, "role": role, "seed": seed,
449
+ "max_tokens": MAX_TOKENS,
450
+ "length_proxy": f"proxy_tokens = whitespace words * {PROXY_FACTOR}", "bucket_rules": BUCKET_RULES,
451
+ "split_rule": "sha256(seed|split|group): even first byte -> dev, odd -> sealed; group = sha256 of the "
452
+ "normalised dolly context (else the normalised instruction / question; long: the book); "
453
+ "one candidate per group; near-duplicates (>50% shared 12-word shingles with the dev sets "
454
+ "or the same split) dropped",
455
+ "selection": ("secret-seeded: groups not used by dev/dev-load (long: books not used), ranked by "
456
+ "sha256(secret|rank|...); the secret is committed by secret_sha256 and revealed after use"
457
+ if secret else "ranked by sha256(seed|rank|split|bucket|key) within the split"),
458
+ "secret_sha256": secret_sha256 if secret else None,
459
+ "secret_rule": SECRET_RULE if secret else None,
460
+ "exclusions": {"rows": sum(len(v) for v in (exclusions or {}).values()),
461
+ "sha256": exclusions_digest(exclusions or {}),
462
+ "rule": "rows listed in exclusions.json or flagged by the private leak-scanner rules"},
463
+ "sources": sources, "builder": "scripts/build_prompts.py (inferencekit prompts build / seal)"}
464
+
465
+
466
+ def write_set(out_dir: Path, name: str, items: list[dict], sealed: bool, role: str, sources: list[dict],
467
+ seed: str = SEED, secret: str | None = None, secret_sha256: str | None = None,
468
+ exclusions: dict | None = None, gz: bool = False) -> dict:
469
+ """Write ``<name>.jsonl`` (``gz``: ``<name>.jsonl.gz``, removing a stale twin) and its manifest.
470
+ The manifest's sha256 is of the uncompressed JSONL bytes either way."""
471
+ out_dir.mkdir(parents=True, exist_ok=True)
472
+ data = jsonl_bytes(items)
473
+ plain, packed = out_dir / f"{name}.jsonl", out_dir / f"{name}.jsonl.gz"
474
+ if gz:
475
+ packed.write_bytes(gzip_bytes(data))
476
+ plain.unlink(missing_ok=True)
477
+ else:
478
+ plain.write_bytes(data)
479
+ packed.unlink(missing_ok=True)
480
+ man = manifest(name, items, data, sealed, role, sources, seed, secret, secret_sha256, exclusions)
481
+ (out_dir / f"{name}.manifest.json").write_text(json.dumps(man, indent=2, ensure_ascii=False) + "\n",
482
+ encoding="utf-8")
483
+ return man
484
+
485
+
486
+ def build_split(cache: Path, out_dir: Path, split: str, seed: str = SEED, secret: str | None = None,
487
+ secret_sha256: str | None = None, exclusions: dict[str, set[str]] | None = None,
488
+ rules: list[re.Pattern] | None = None, gz: bool = False) -> tuple[list[dict], dict[str, set[str]]]:
489
+ """Write ``<split>.jsonl`` and ``<split>-load.jsonl`` (``gz``: ``.jsonl.gz``) (+ manifests) into ``out_dir``.
490
+
491
+ Returns (manifests, exclusions applied)."""
492
+ cands, applied = candidates(cache, seed, exclusions, rules)
493
+ sets = select(cands, split, seed, secret)
494
+ sources = source_table(cache)
495
+ sealed = split == "sealed"
496
+ mans = [write_set(out_dir, split, sets["single"], sealed, "single", sources, seed, secret, secret_sha256, applied,
497
+ gz=gz),
498
+ write_set(out_dir, f"{split}-load", sets["load"], sealed, "load", sources, seed, secret, secret_sha256,
499
+ applied, gz=gz)]
500
+ return mans, applied
501
+
502
+
503
+ def rebuild_sha256(cache: Path, seed: str, secret: str | None, exclusions: dict[str, set[str]] | None,
504
+ rules: list[re.Pattern] | None = None) -> dict[str, str]:
505
+ """{'single': sha256, 'load': sha256} of the sealed sets the given secret selects (nothing written)."""
506
+ cands, _ = candidates(cache, seed, exclusions, rules)
507
+ sets = select(cands, "sealed", seed, secret)
508
+ return {role: hashlib.sha256(jsonl_bytes(items)).hexdigest() for role, items in sets.items()}
509
+
510
+
511
+ def write_sums(out_dir: Path, manifests: list[dict]) -> Path:
512
+ path = out_dir / "SHA256SUMS"
513
+ lines = [f"{m['sha256']} {m['name']}.jsonl\n" for m in manifests]
514
+ for m in manifests:
515
+ mp = out_dir / f"{m['name']}.manifest.json"
516
+ lines.append(f"{sha256_file(mp)} {mp.name}\n")
517
+ path.write_text("".join(lines), encoding="utf-8")
518
+ return path
Binary file