quantum-cortex 1.0.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.
- cortex_c2/__init__.py +161 -0
- cortex_c2/consolidation.py +146 -0
- cortex_c2/router_ablation.py +254 -0
- cortex_c2/router_v2.py +109 -0
- cortex_c2b/__init__.py +724 -0
- cortex_c2b/companion_demo.py +203 -0
- cortex_c2b/crypto.py +102 -0
- cortex_c2b/external_arm.py +591 -0
- cortex_c2b/hm_lm.py +983 -0
- cortex_c2b/hm_protocol.py +308 -0
- cortex_c2b/lifecycle.py +454 -0
- cortex_c2b/lm_bridge.py +338 -0
- cortex_c2b/organ_use.py +254 -0
- cortex_c2b/read_path.py +218 -0
- cortex_c2b/reconstruction_probe.py +471 -0
- cortex_c2b/write_path.py +176 -0
- cortex_data/__init__.py +439 -0
- cortex_data/relay.py +174 -0
- cortex_eval/__init__.py +20 -0
- cortex_eval/ablation_mqar.py +187 -0
- cortex_eval/confirmation_run.py +133 -0
- cortex_eval/domain_map.py +127 -0
- cortex_eval/external_baseline.py +148 -0
- cortex_eval/ingest_confirmation.py +194 -0
- cortex_eval/mqar.py +122 -0
- cortex_eval/multiseed.py +148 -0
- cortex_eval/recurrent_base.py +197 -0
- cortex_eval/recurrent_ladder.py +159 -0
- cortex_eval/resumable_ladder.py +231 -0
- cortex_eval/resumable_rb.py +207 -0
- cortex_eval/run_mqar.py +127 -0
- cortex_eval/serial_position.py +116 -0
- quantum_cortex-1.0.0.dist-info/METADATA +209 -0
- quantum_cortex-1.0.0.dist-info/RECORD +41 -0
- quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
- quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
- quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
- quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
- train.py +885 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""cortex_c2b.external_arm -- the Molaison LM arm on an EXTERNAL model through a
|
|
2
|
+
prompt adapter (ADR-008, amendment of 2026-09-16, declared before any measurement).
|
|
3
|
+
|
|
4
|
+
The Molaison dissociation (the H.M. protocol: named after Henry Molaison, the
|
|
5
|
+
patient whose hippocampus was removed in 1953 and who formed no new episodic
|
|
6
|
+
memory from that day while keeping every skill he had; the protocol asks the
|
|
7
|
+
model the same question: with its journal cut, do the episodes vanish while the
|
|
8
|
+
skills stay?) is run here on a model that was never trained with the organ: an
|
|
9
|
+
open weight causal language model people use, at three sizes, through a FROZEN
|
|
10
|
+
prompt whose hash enters every record. Door 2 of whitepaper section 5d.
|
|
11
|
+
|
|
12
|
+
What does not change (the frozen protocol): the generator and its seeds (0 and
|
|
13
|
+
10 000), the organ (session A plants through the gated write path into a journal
|
|
14
|
+
sealed on disk; session B reopens it from the disk alone in a NEW process), the
|
|
15
|
+
read window (k = 4 lines, 256 bytes, `A:<payload>` lines in a seeded random
|
|
16
|
+
order, labels A to H), the contract (cite a label and give the attribute, or
|
|
17
|
+
abstain), the counting rules of `hm_lm._probe`, the thresholds (d 0.50, e_S 1 %,
|
|
18
|
+
floor chance + 5 points, invalid citations at most 1 %, claims on never planted
|
|
19
|
+
entities under 10 %), the verdict, `persistent` and `claimable`.
|
|
20
|
+
|
|
21
|
+
What changes, declared (the amendment says why):
|
|
22
|
+
1. The cue is the organ's own hash seeded encoder (the entity as the address,
|
|
23
|
+
rows 13 and 13b): the retrieval hit is the organ's number, not the model's.
|
|
24
|
+
2. The window enters as TEXT in a frozen frame (rules, four worked examples
|
|
25
|
+
from the training facts family, the notebook lines verbatim, the question);
|
|
26
|
+
the frame's sha256 (16 hex) is the prompt hash; the same frame for every
|
|
27
|
+
size; a change is a new hash and a new row.
|
|
28
|
+
3. The contract's surface is text: one line, `UNKNOWN` or
|
|
29
|
+
`CITE <label> ANS <attribute>`; the adapter maps it onto the contract's
|
|
30
|
+
tokens and hands them to `parse_contract` and to the same counting rules;
|
|
31
|
+
any other shape is malformed, a claim counted against the model.
|
|
32
|
+
4. Greedy decoding, at most 32 new tokens, the published weights in their
|
|
33
|
+
published dtype (bf16), no quantisation, no training, no adapter weights;
|
|
34
|
+
the model's name, revision, dtype, template and hardware in the file.
|
|
35
|
+
5. Arm S: 256 spans of 1024 bytes from the validation part of the N1 slice;
|
|
36
|
+
OFF the span alone, ON the notebook block the organ returns for the span's
|
|
37
|
+
own cue followed by the span; the loss on the span's tokens after its
|
|
38
|
+
first one, in both arms; `hm_skill_delta_frame` (the frame's fixed text and
|
|
39
|
+
an empty notebook before the span) as attribution, not as a condition.
|
|
40
|
+
6. Session B in a new process; a second execution must agree number for
|
|
41
|
+
number (`--agree`); both files retained.
|
|
42
|
+
|
|
43
|
+
Nothing here is a claim. The first number comes from the first file.
|
|
44
|
+
"""
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import argparse
|
|
48
|
+
import hashlib
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import platform
|
|
52
|
+
import re
|
|
53
|
+
import sys
|
|
54
|
+
from datetime import datetime, timezone
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
from cortex_c2b import Journal, MODE_DURABLE, POLICY_STOP, lifecycle_declaration, content_hash
|
|
60
|
+
from cortex_c2b.hm_protocol import (Fact, generate_facts, _cue_for, _TEMPLATES, _ATTRS, SCHEMAS, DELTA, EPSILON_S,
|
|
61
|
+
FLOOR_MARGIN, NEGCTRL_INVALID, N_FACTS, N_NEGCTRL)
|
|
62
|
+
from cortex_c2b.lm_bridge import (build_read_window, parse_contract, LABELS, NEWLINE, TOK_CITE, TOK_ANS,
|
|
63
|
+
TOK_UNKNOWN)
|
|
64
|
+
from cortex_c2b.hm_lm import INVALID_CITATION_MAX
|
|
65
|
+
from cortex_c2b.read_path import JournalPath
|
|
66
|
+
from cortex_c2b.write_path import WritePath
|
|
67
|
+
|
|
68
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
69
|
+
|
|
70
|
+
# ---- declared constants (the amendment of 2026-09-16) ------------------------
|
|
71
|
+
K = 4 # lines in a read window, as the cortex's arm
|
|
72
|
+
BUDGET_BYTES = 256 # the window's byte budget, as the cortex's arm
|
|
73
|
+
MAX_NEW_TOKENS = 32 # greedy decoding, stopped at the first newline
|
|
74
|
+
N_SPANS = 256 # arm S: 16 batches of 16 spans, as the trainer draws them
|
|
75
|
+
SPAN_BYTES = 1024 # arm S: the trainer's block size
|
|
76
|
+
VAL_FRACTION = 0.01 # the N1 validation split (train.py, `val_fraction`)
|
|
77
|
+
EXAMPLES_SEED = 100_000 # the training facts family (organ_use), disjoint from the protocol's seeds
|
|
78
|
+
EXAMPLES_SHUFFLE_SEED = 100_002
|
|
79
|
+
N_EXAMPLES = 4 # two citations, two abstentions, in a fixed order
|
|
80
|
+
FRAME_DIR = REPO_ROOT / "docs" / "benchmarks"
|
|
81
|
+
DATA_BIN = REPO_ROOT / "data" / "fineweb_edu_bytes.bin"
|
|
82
|
+
PROVENANCE = REPO_ROOT / "data" / "mixes" / "fineweb-edu-n1.provenance.json"
|
|
83
|
+
NOTEBOOK_HEADER = "Notebook:\n"
|
|
84
|
+
|
|
85
|
+
# the three sizes pinned in the addendum of 2026-09-17 (ids and revisions verified on the hub that day)
|
|
86
|
+
PINNED_MODELS = {
|
|
87
|
+
"Qwen/Qwen3-1.7B": "70d244cc86ccca08cf5af4e1e306ecf908b1ad5e",
|
|
88
|
+
"Qwen/Qwen3-4B": "1cfa9a7208912126459214e8b04321603b3df60c",
|
|
89
|
+
"Qwen/Qwen3-8B": "b968826d9c46dd6066d109eabc6255188de91218",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_CITE_RE = re.compile(r"^CITE ([A-H]) ANS (.+)$")
|
|
93
|
+
_ENTITY_RE = re.compile(r"[A-Z][a-z]{3}-[0-9a-f]{4}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------- #
|
|
97
|
+
# The frame: written once, retained as a file, its hash in every record #
|
|
98
|
+
# ---------------------------------------------------------------------------- #
|
|
99
|
+
RULES = (
|
|
100
|
+
"You are given a notebook of records. Each record is one line that starts with a letter label and a colon.\n"
|
|
101
|
+
"Answer the question from the notebook only.\n"
|
|
102
|
+
"If one record is about the entity named in the question, answer with exactly one line of this form:\n"
|
|
103
|
+
"CITE <label> ANS <attribute>\n"
|
|
104
|
+
"where <label> is that record's letter and <attribute> is copied word for word from that record.\n"
|
|
105
|
+
"If no record is about that entity, answer with exactly one line:\n"
|
|
106
|
+
"UNKNOWN\n"
|
|
107
|
+
"Write nothing else. Never guess.\n"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _example_items(facts: list[Fact], idx: list[int]) -> list[tuple[str, bytes]]:
|
|
112
|
+
return [(content_hash(facts[i].statement.encode("utf-8")), facts[i].statement.encode("utf-8")) for i in idx]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def worked_examples() -> list[dict]:
|
|
116
|
+
"""Four examples from the training facts family (`organ_use.training_facts`,
|
|
117
|
+
seed 100 000, decontaminated against the protocol's entities): two
|
|
118
|
+
citations and two abstentions, in a fixed order; their notebooks are built
|
|
119
|
+
by `build_read_window`, the protocol's own window builder."""
|
|
120
|
+
from cortex_c2b.organ_use import training_facts
|
|
121
|
+
facts, negs, _ = training_facts(40, seed=EXAMPLES_SEED)
|
|
122
|
+
rng = np.random.default_rng(EXAMPLES_SHUFFLE_SEED)
|
|
123
|
+
per = len(facts) // len(SCHEMAS)
|
|
124
|
+
plan = [("cite", facts[0], [0, 1, per, 2 * per]), # a person, among three others
|
|
125
|
+
("abstain", negs[0], [3, per + 1, 2 * per + 1, 3 * per]), # never planted: four others
|
|
126
|
+
("cite", facts[2 * per], [2 * per, 3 * per + 1, 4 * per, 4]), # an event, among three others
|
|
127
|
+
("abstain", negs[len(negs) // 5], [5, per + 2, 3 * per + 2, 4 * per + 1])] # a place, another schema
|
|
128
|
+
out = []
|
|
129
|
+
for kind, f, idx in plan:
|
|
130
|
+
window, labels = build_read_window(_example_items(facts, idx), BUDGET_BYTES, rng)
|
|
131
|
+
if kind == "cite":
|
|
132
|
+
own = content_hash(f.statement.encode("utf-8"))
|
|
133
|
+
label = next(chr(l) for l, p in labels.items() if p == own)
|
|
134
|
+
answer = f"CITE {label} ANS {f.attr}"
|
|
135
|
+
else:
|
|
136
|
+
answer = "UNKNOWN"
|
|
137
|
+
out.append({"window": window.decode("utf-8"), "question": f.query, "answer": answer, "kind": kind})
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def build_frame() -> str:
|
|
142
|
+
"""The frozen frame with two placeholders, `{window}` and `{question}`."""
|
|
143
|
+
parts = [RULES, "\n"]
|
|
144
|
+
for i, ex in enumerate(worked_examples(), 1):
|
|
145
|
+
parts.append(f"Example {i}\n{NOTEBOOK_HEADER}{ex['window']}Question: {ex['question']}\nAnswer: {ex['answer']}\n\n")
|
|
146
|
+
parts.append(f"{NOTEBOOK_HEADER}{{window}}Question: {{question}}\nAnswer:")
|
|
147
|
+
return "".join(parts)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def frame_hash(frame: str) -> str:
|
|
151
|
+
return hashlib.sha256(frame.encode("utf-8")).hexdigest()[:16]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def frame_path(frame: str) -> Path:
|
|
155
|
+
return FRAME_DIR / f"external-arm-prompt-{frame_hash(frame)}.txt"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def render(frame: str, window: bytes, question: str) -> str:
|
|
159
|
+
"""The window's lines verbatim under the header (an empty window leaves the
|
|
160
|
+
block empty), then the question. `replace`, never `format`: the notebook may
|
|
161
|
+
hold any byte."""
|
|
162
|
+
return frame.replace("{window}", window.decode("utf-8", errors="replace")).replace("{question}", question)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def frame_prefix(frame: str) -> str:
|
|
166
|
+
"""The frame's fixed text up to the empty notebook block (arm S diagnostic)."""
|
|
167
|
+
return frame[: frame.index("{window}")]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------- #
|
|
171
|
+
# The contract's surface: text in, the contract's tokens out, the same parser #
|
|
172
|
+
# ---------------------------------------------------------------------------- #
|
|
173
|
+
def first_line(completion: str) -> str:
|
|
174
|
+
"""The completion's first line after leading whitespace is dropped, its
|
|
175
|
+
outer whitespace stripped and nothing else (no case folding)."""
|
|
176
|
+
return completion.lstrip().split("\n", 1)[0].strip()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def contract_tokens(completion: str) -> list[int]:
|
|
180
|
+
"""Map the model's line onto the contract's tokens: `UNKNOWN` -> [<UNKNOWN>,
|
|
181
|
+
newline]; `CITE <label> ANS <attribute>` -> [<CITE>, label, <ANS>, attribute
|
|
182
|
+
bytes, newline]; anything else -> its own bytes, which `parse_contract`
|
|
183
|
+
reads as malformed (a claim counted against the model)."""
|
|
184
|
+
line = first_line(completion)
|
|
185
|
+
if line == "UNKNOWN":
|
|
186
|
+
return [TOK_UNKNOWN, NEWLINE]
|
|
187
|
+
m = _CITE_RE.match(line)
|
|
188
|
+
if m:
|
|
189
|
+
return [TOK_CITE, ord(m.group(1)), TOK_ANS] + list(m.group(2).encode("utf-8")) + [NEWLINE]
|
|
190
|
+
return list(line.encode("utf-8")) + [NEWLINE]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def score(kinds: list[tuple[str, int | None, str | None]], facts: list[Fact], labels_of: list[dict[int, str]],
|
|
194
|
+
pointers_of: list[list[str]], payloads, pointer_of: dict[str, str], journal_on: bool) -> dict:
|
|
195
|
+
"""The counting rules of `hm_lm._probe`, line for line, on parsed answers.
|
|
196
|
+
`labels_of[i]`: label byte -> pointer of the i th window (empty when OFF);
|
|
197
|
+
`pointers_of[i]`: what the organ returned; `pointer_of`: entity -> pointer
|
|
198
|
+
for planted facts (empty for the negative control)."""
|
|
199
|
+
n = max(1, len(facts))
|
|
200
|
+
strict = abstain = invalid = valid = attr_ok = guess = attr_by_chance = hit = malformed = 0
|
|
201
|
+
for f, (kind, label, attr), labels, pointers in zip(facts, kinds, labels_of, pointers_of):
|
|
202
|
+
if journal_on and pointer_of:
|
|
203
|
+
hit += int(pointer_of.get(f.entity) in pointers)
|
|
204
|
+
if kind == "unknown":
|
|
205
|
+
abstain += 1
|
|
206
|
+
continue
|
|
207
|
+
guess += 1
|
|
208
|
+
if kind == "malformed":
|
|
209
|
+
malformed += 1
|
|
210
|
+
if kind == "cite" and label in labels:
|
|
211
|
+
payload = payloads.get(labels[label]) if labels[label] in payloads else b""
|
|
212
|
+
cited_ok = (attr or "").encode("utf-8") in payload and len(attr or "") > 0
|
|
213
|
+
if cited_ok:
|
|
214
|
+
valid += 1
|
|
215
|
+
if attr == f.attr:
|
|
216
|
+
strict += 1; attr_ok += 1
|
|
217
|
+
else:
|
|
218
|
+
invalid += 1 # a label that exists but does not hold the claim
|
|
219
|
+
else:
|
|
220
|
+
invalid += 1 # a citation of nothing, or a malformed claim
|
|
221
|
+
if attr == f.attr:
|
|
222
|
+
attr_by_chance += 1
|
|
223
|
+
return {"recall_strict": strict / n, "abstain_rate": abstain / n, "guess_rate": guess / n,
|
|
224
|
+
"invalid_citation_rate": invalid / n, "valid_citation_rate": valid / n,
|
|
225
|
+
"attr_exact_given_valid": (attr_ok / valid) if valid else None,
|
|
226
|
+
"attr_hit_any": attr_by_chance / n, "malformed_rate": malformed / n,
|
|
227
|
+
"retrieval_hit": (hit / n) if (journal_on and pointer_of) else None}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _progress(msg: str) -> None:
|
|
231
|
+
"""One dated line on stderr; the log keeps it, the file does not."""
|
|
232
|
+
print(f"[{datetime.now(timezone.utc).strftime('%H:%M:%S')}] {msg}", file=sys.stderr, flush=True)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------- #
|
|
236
|
+
# The organ: session A plants (no model), session B reopens in a new process #
|
|
237
|
+
# ---------------------------------------------------------------------------- #
|
|
238
|
+
def _key():
|
|
239
|
+
from cortex_c2b.crypto import key_from_env
|
|
240
|
+
k = key_from_env()
|
|
241
|
+
if k is None:
|
|
242
|
+
raise SystemExit("QUANTUM_CORTEX_JOURNAL_KEY is not set: the journal is sealed with it (32 bytes hex)")
|
|
243
|
+
return k
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def journal_file(journal_dir: Path) -> Path:
|
|
247
|
+
return Path(journal_dir) / "journal.jsonl"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def plant(journal_dir: Path, n_facts: int = N_FACTS, seed: int = 0, key: bytes | None = None) -> dict:
|
|
251
|
+
"""Session A: the frozen protocol's facts through the NORMAL gated write
|
|
252
|
+
path, cues from the organ's own encoder (the entity as the address), into a
|
|
253
|
+
journal sealed on disk under the `stop` policy. Writes a sidecar with what
|
|
254
|
+
session B needs to declare persistence."""
|
|
255
|
+
journal_dir = Path(journal_dir); journal_dir.mkdir(parents=True, exist_ok=True)
|
|
256
|
+
key = key or _key()
|
|
257
|
+
facts, gen_hash = generate_facts(n_facts, seed)
|
|
258
|
+
journal = Journal(journal_file(journal_dir), key=key, policy=POLICY_STOP)
|
|
259
|
+
wp = WritePath(journal, seed=seed)
|
|
260
|
+
jp = JournalPath(journal, seed=seed)
|
|
261
|
+
admitted = 0
|
|
262
|
+
for i, f in enumerate(facts):
|
|
263
|
+
rep = wp.write(f.cue, f.statement.encode("utf-8"), f.schema, now=float(i))
|
|
264
|
+
if rep.admitted:
|
|
265
|
+
jp.on_write(rep.entry); admitted += 1
|
|
266
|
+
side = {"session": "A", "generator": {"config_hash": gen_hash, "n_facts": n_facts, "seed": seed},
|
|
267
|
+
"admitted": admitted, "durable": journal.mode == MODE_DURABLE,
|
|
268
|
+
"storage": lifecycle_declaration(journal)["storage"], "timestamp_utc": datetime.now(timezone.utc).isoformat()}
|
|
269
|
+
(journal_dir / "session-a.json").write_text(json.dumps(side, indent=2) + "\n", encoding="utf-8", newline="\n")
|
|
270
|
+
return side
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def load_val_spans(bin_path: Path = DATA_BIN, n_spans: int = N_SPANS, span_bytes: int = SPAN_BYTES, seed: int = 0,
|
|
274
|
+
val_fraction: float = VAL_FRACTION) -> tuple[list[str], dict]:
|
|
275
|
+
"""Arm S text: spans of the validation part of the N1 slice (uint16 byte
|
|
276
|
+
tokens; the reserved ids 256 to 263 are dropped), decoded as UTF-8 with
|
|
277
|
+
undecodable bytes replaced, drawn with a fixed seed, the same for every model."""
|
|
278
|
+
arr = np.memmap(bin_path, dtype=np.uint16, mode="r")
|
|
279
|
+
split = int(len(arr) * (1.0 - val_fraction))
|
|
280
|
+
val = arr[split:]
|
|
281
|
+
rng = np.random.default_rng(seed)
|
|
282
|
+
starts = rng.integers(0, len(val) - span_bytes, size=n_spans)
|
|
283
|
+
spans = []
|
|
284
|
+
for s in starts:
|
|
285
|
+
raw = np.asarray(val[int(s): int(s) + span_bytes])
|
|
286
|
+
spans.append(bytes(int(v) for v in raw if v < 256).decode("utf-8", errors="replace"))
|
|
287
|
+
prov = None
|
|
288
|
+
if PROVENANCE.exists():
|
|
289
|
+
try:
|
|
290
|
+
prov = json.loads(PROVENANCE.read_text(encoding="utf-8")).get("content_sha")
|
|
291
|
+
except (OSError, ValueError):
|
|
292
|
+
prov = None
|
|
293
|
+
return spans, {"path": str(bin_path), "n_bytes": int(len(arr)), "val_fraction": val_fraction, "n_spans": n_spans,
|
|
294
|
+
"span_bytes": span_bytes, "seed": seed, "provenance_content_sha": prov}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _arm(model, frame: str, jp: JournalPath | None, facts: list[Fact], rng, payloads, pointer_of: dict[str, str],
|
|
298
|
+
journal_on: bool) -> tuple[dict, list[dict]]:
|
|
299
|
+
"""One arm over a fact list: the organ reads (or not), the frame is rendered,
|
|
300
|
+
the model completes, the line is mapped and parsed, the answers are kept."""
|
|
301
|
+
kinds, labels_of, pointers_of, transcript = [], [], [], []
|
|
302
|
+
arm_name = ("on" if journal_on else "off") + ("" if pointer_of else " (never planted)")
|
|
303
|
+
for i, f in enumerate(facts):
|
|
304
|
+
if i % 50 == 0:
|
|
305
|
+
_progress(f"arm E {arm_name}: {i}/{len(facts)}")
|
|
306
|
+
if journal_on:
|
|
307
|
+
hits = jp.retrieve(f.cue, k=K)
|
|
308
|
+
items = [(e.pointer, payload) for e, payload, _ in hits]
|
|
309
|
+
window, labels = build_read_window(items, BUDGET_BYTES, rng)
|
|
310
|
+
pointers = [p for p, _ in items]
|
|
311
|
+
else:
|
|
312
|
+
window, labels, pointers = b"", {}, []
|
|
313
|
+
completion = model.complete(render(frame, window, f.query))
|
|
314
|
+
toks = contract_tokens(completion)
|
|
315
|
+
kind, label, attr = parse_contract(toks)
|
|
316
|
+
kinds.append((kind, label, attr)); labels_of.append(labels); pointers_of.append(pointers)
|
|
317
|
+
transcript.append({"entity": f.entity, "schema": f.schema, "attr": f.attr, "line": first_line(completion),
|
|
318
|
+
"kind": kind, "label": (chr(label) if label else None), "answered": attr,
|
|
319
|
+
"own_in_window": (pointer_of.get(f.entity) in pointers) if pointer_of else None})
|
|
320
|
+
return score(kinds, facts, labels_of, pointers_of, payloads, pointer_of, journal_on), transcript
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def skill_arm(model, frame: str, jp: JournalPath, spans: list[str], rng) -> dict:
|
|
324
|
+
"""Arm S. OFF: the span alone. ON: the notebook block the organ returns for
|
|
325
|
+
the span's own cue, then the span. Diagnostic: the frame's fixed text and an
|
|
326
|
+
empty notebook, then the span. The loss on the span's tokens after its
|
|
327
|
+
first one, in every arm; perplexity = exp(total nll / total tokens)."""
|
|
328
|
+
tot = {"off": [0.0, 0], "on": [0.0, 0], "frame": [0.0, 0]}
|
|
329
|
+
pre_frame = frame_prefix(frame) # ends with the notebook header, the block empty
|
|
330
|
+
for i, text in enumerate(spans):
|
|
331
|
+
if i % 64 == 0:
|
|
332
|
+
_progress(f"arm S: {i}/{len(spans)} spans")
|
|
333
|
+
hits = jp.retrieve(_cue_for(text), k=K)
|
|
334
|
+
window, _ = build_read_window([(e.pointer, p) for e, p, _ in hits], BUDGET_BYTES, rng)
|
|
335
|
+
block = NOTEBOOK_HEADER + window.decode("utf-8", errors="replace")
|
|
336
|
+
for name, prefix in (("off", ""), ("on", block), ("frame", pre_frame)):
|
|
337
|
+
s, n = model.nll(prefix, text)
|
|
338
|
+
tot[name][0] += float(s); tot[name][1] += int(n)
|
|
339
|
+
ppl = {k: (float(np.exp(v[0] / v[1])) if v[1] else None) for k, v in tot.items()}
|
|
340
|
+
delta = max(0.0, (ppl["on"] - ppl["off"]) / ppl["off"]) if ppl["off"] else 0.0
|
|
341
|
+
delta_frame = max(0.0, (ppl["frame"] - ppl["off"]) / ppl["off"]) if ppl["off"] else 0.0
|
|
342
|
+
return {"hm_skill_delta": delta, "ppl_on": ppl["on"], "ppl_off": ppl["off"],
|
|
343
|
+
"hm_skill_delta_frame": delta_frame, "ppl_frame": ppl["frame"], "tokens_scored": tot["off"][1]}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def probe(model, journal_dir: Path, frame: str | None = None, n_facts: int = N_FACTS, n_negctrl: int = N_NEGCTRL,
|
|
347
|
+
seed: int = 0, spans: list[str] | None = None, data_note: dict | None = None,
|
|
348
|
+
key: bytes | None = None) -> dict:
|
|
349
|
+
"""Session B: nothing of session A but the disk. Reopens the sealed journal,
|
|
350
|
+
runs the three arms of arm E and arm S, and returns the file's content."""
|
|
351
|
+
journal_dir = Path(journal_dir)
|
|
352
|
+
frame = frame or build_frame()
|
|
353
|
+
key = key or _key()
|
|
354
|
+
side = json.loads((journal_dir / "session-a.json").read_text(encoding="utf-8"))
|
|
355
|
+
journal = Journal(journal_file(journal_dir), key=key, policy=POLICY_STOP)
|
|
356
|
+
jp = JournalPath(journal, seed=seed)
|
|
357
|
+
rng = np.random.default_rng(seed + 1) # the window order, as session B of the cortex's arm
|
|
358
|
+
facts, gen_hash = generate_facts(n_facts, seed)
|
|
359
|
+
neg, _ = generate_facts(n_negctrl, seed + 10_000)
|
|
360
|
+
pointer_of = {f.entity: content_hash(f.statement.encode("utf-8")) for f in facts
|
|
361
|
+
if content_hash(f.statement.encode("utf-8")) in journal.payloads}
|
|
362
|
+
chance = float(np.mean([1.0 / len(_ATTRS[s]) for s in SCHEMAS]))
|
|
363
|
+
floor = chance + FLOOR_MARGIN
|
|
364
|
+
|
|
365
|
+
on, t_on = _arm(model, frame, jp, facts, rng, journal.payloads, pointer_of, True)
|
|
366
|
+
off, t_off = _arm(model, frame, None, facts, rng, journal.payloads, pointer_of, False)
|
|
367
|
+
neg_on, t_neg = _arm(model, frame, jp, neg, rng, journal.payloads, {}, True)
|
|
368
|
+
neg_off, _ = _arm(model, frame, None, neg, rng, journal.payloads, {}, False)
|
|
369
|
+
if spans is None:
|
|
370
|
+
spans, data_note = load_val_spans(seed=seed)
|
|
371
|
+
skills = skill_arm(model, frame, jp, spans, rng)
|
|
372
|
+
|
|
373
|
+
storage = lifecycle_declaration(journal)["storage"]
|
|
374
|
+
persistent = bool(side.get("durable") and journal.mode == MODE_DURABLE)
|
|
375
|
+
negctrl_claims = neg_on["guess_rate"]
|
|
376
|
+
valid = negctrl_claims < NEGCTRL_INVALID
|
|
377
|
+
dissociation = (skills["hm_skill_delta"] <= EPSILON_S and (on["recall_strict"] - off["recall_strict"]) >= DELTA
|
|
378
|
+
and off["recall_strict"] <= floor and on["invalid_citation_rate"] <= INVALID_CITATION_MAX and valid)
|
|
379
|
+
desc = model.describe()
|
|
380
|
+
return {
|
|
381
|
+
"benchmark": "hm_protocol_lm_external",
|
|
382
|
+
"spec": ("docs/benchmarks/hm-protocol.md (frozen thresholds; LM arm amendment 2026-09-12); "
|
|
383
|
+
"ADR-008 amendment 2026-09-16 (the external model arm, declared before any measurement)"),
|
|
384
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
385
|
+
"model": desc,
|
|
386
|
+
"prompt": {"hash": frame_hash(frame), "file": f"docs/benchmarks/external-arm-prompt-{frame_hash(frame)}.txt",
|
|
387
|
+
"examples_seed": EXAMPLES_SEED, "decoding": "greedy", "max_new_tokens": MAX_NEW_TOKENS},
|
|
388
|
+
"thresholds": {"delta": DELTA, "epsilon_s": EPSILON_S, "floor": floor, "chance": chance,
|
|
389
|
+
"negctrl_invalid": NEGCTRL_INVALID, "invalid_citation_max": INVALID_CITATION_MAX},
|
|
390
|
+
"generator": {"config_hash": gen_hash, "n_facts": n_facts, "n_negctrl": n_negctrl, "seed": seed,
|
|
391
|
+
"admitted": side.get("admitted"), "planted_found": len(pointer_of)},
|
|
392
|
+
"window": {"k": K, "budget_bytes": BUDGET_BYTES, "shuffle_seed": seed + 1,
|
|
393
|
+
"cue": "the organ's hash seeded encoder, the entity as the address (rows 13, 13b)"},
|
|
394
|
+
"hm_recall_on": on["recall_strict"], "hm_recall_off": off["recall_strict"],
|
|
395
|
+
"hm_gap": on["recall_strict"] - off["recall_strict"],
|
|
396
|
+
**skills,
|
|
397
|
+
"hm_negctrl_rate": negctrl_claims, "hm_negctrl_on": neg_on["attr_hit_any"],
|
|
398
|
+
"hm_negctrl_abstain_on": neg_on["abstain_rate"], "hm_negctrl_abstain_off": neg_off["abstain_rate"],
|
|
399
|
+
"hm_false_abstention_on": on["abstain_rate"], "hm_invalid_citation_on": on["invalid_citation_rate"],
|
|
400
|
+
"hm_valid_citation_on": on["valid_citation_rate"], "hm_attr_exact_given_valid": on["attr_exact_given_valid"],
|
|
401
|
+
"hm_retrieval_hit": on["retrieval_hit"], "hm_attention_mass": None,
|
|
402
|
+
"hm_guess_rate_off": off["guess_rate"], "hm_attr_hit_off_by_chance": off["attr_hit_any"],
|
|
403
|
+
"hm_malformed_on": on["malformed_rate"], "hm_malformed_off": off["malformed_rate"],
|
|
404
|
+
"hm_malformed_negctrl": neg_on["malformed_rate"],
|
|
405
|
+
"run_valid": valid, "hm_dissociation_pass": int(dissociation),
|
|
406
|
+
"persistent": persistent, "claimable": int(dissociation and persistent),
|
|
407
|
+
"storage": {"on_disk": True, "mode": storage["mode"], "policy": storage["policy"],
|
|
408
|
+
"plaintext_scope": storage["plaintext_scope"]},
|
|
409
|
+
"attribution": {"encoder": on["retrieval_hit"], "encoder_note": "the organ's number (the entity as the address)",
|
|
410
|
+
"reader": None, "reader_note": "not observable on an external model",
|
|
411
|
+
"answer": on["attr_exact_given_valid"]},
|
|
412
|
+
"skill_arm": {"data": data_note, "n_spans": len(spans)},
|
|
413
|
+
"session_a": side, "process": "new (nothing of session A but the disk)",
|
|
414
|
+
"answers": {"on": t_on, "off": t_off, "negctrl": t_neg},
|
|
415
|
+
"verdict": ((("PASS" if dissociation else ("INVALID" if not valid else "FAIL"))
|
|
416
|
+
+ " -- external model arm, cite-or-abstain contract through the frozen prompt")
|
|
417
|
+
+ ("" if persistent else " -- persistent: false, not claimable")),
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# ---------------------------------------------------------------------------- #
|
|
422
|
+
# The models: a Hugging Face causal LM behind the two calls the arm needs #
|
|
423
|
+
# ---------------------------------------------------------------------------- #
|
|
424
|
+
def model_slug(name: str, template: str) -> str:
|
|
425
|
+
slug = re.sub(r"[^a-z0-9.]+", "-", name.lower()).strip("-")
|
|
426
|
+
return slug + ("-raw" if template == "raw" else "")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class HFCausalModel:
|
|
430
|
+
"""`transformers` is imported here and nowhere else: the fast suite never
|
|
431
|
+
needs it. bf16 only, the published dtype; a card without bf16 stops the run
|
|
432
|
+
instead of changing the dtype (a quantised or fp16 run would be a second
|
|
433
|
+
variable)."""
|
|
434
|
+
|
|
435
|
+
def __init__(self, name: str, revision: str, device: str = "cpu", chat: bool = True,
|
|
436
|
+
max_new_tokens: int = MAX_NEW_TOKENS):
|
|
437
|
+
import torch
|
|
438
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
439
|
+
import transformers
|
|
440
|
+
if device.startswith("cuda") and not torch.cuda.is_bf16_supported():
|
|
441
|
+
raise SystemExit("this card has no bf16: the arm is not run in another dtype (amendment 2026-09-16)")
|
|
442
|
+
self.torch = torch
|
|
443
|
+
self.name, self.revision, self.chat, self.max_new_tokens = name, revision, chat, max_new_tokens
|
|
444
|
+
self.device = torch.device(device)
|
|
445
|
+
self.tok = AutoTokenizer.from_pretrained(name, revision=revision)
|
|
446
|
+
major, minor = (int(x) for x in transformers.__version__.split(".")[:2])
|
|
447
|
+
dtype_kw = {"dtype": torch.bfloat16} if (major, minor) >= (4, 56) else {"torch_dtype": torch.bfloat16}
|
|
448
|
+
self.model = AutoModelForCausalLM.from_pretrained(name, revision=revision, **dtype_kw)
|
|
449
|
+
self.model.to(self.device).eval()
|
|
450
|
+
if next(self.model.parameters()).dtype != torch.bfloat16:
|
|
451
|
+
raise SystemExit("the weights did not load in bf16: the arm is not run in another dtype")
|
|
452
|
+
self.versions = {"transformers": transformers.__version__, "torch": torch.__version__}
|
|
453
|
+
self.thinking = False if chat else None
|
|
454
|
+
|
|
455
|
+
def describe(self) -> dict:
|
|
456
|
+
t = self.torch
|
|
457
|
+
hw = t.cuda.get_device_name(0) if self.device.type == "cuda" else platform.processor() or platform.machine()
|
|
458
|
+
return {"name": self.name, "revision": self.revision, "dtype": "bf16",
|
|
459
|
+
"template": "chat" if self.chat else "raw", "thinking": self.thinking,
|
|
460
|
+
"tokenizer": self.tok.__class__.__name__, "vocab_size": len(self.tok),
|
|
461
|
+
"device": str(self.device), "hardware": hw, "versions": self.versions,
|
|
462
|
+
"chat_template_sha256_16": (hashlib.sha256(self.tok.chat_template.encode("utf-8")).hexdigest()[:16]
|
|
463
|
+
if (self.chat and getattr(self.tok, "chat_template", None)) else None)}
|
|
464
|
+
|
|
465
|
+
def _prompt_ids(self, prompt: str):
|
|
466
|
+
if self.chat:
|
|
467
|
+
text = self.tok.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False,
|
|
468
|
+
add_generation_prompt=True, enable_thinking=False)
|
|
469
|
+
return self.tok(text, return_tensors="pt", add_special_tokens=False).input_ids
|
|
470
|
+
return self.tok(prompt, return_tensors="pt").input_ids
|
|
471
|
+
|
|
472
|
+
def complete(self, prompt: str) -> str:
|
|
473
|
+
t = self.torch
|
|
474
|
+
ids = self._prompt_ids(prompt).to(self.device)
|
|
475
|
+
with t.no_grad():
|
|
476
|
+
out = self.model.generate(ids, max_new_tokens=self.max_new_tokens, do_sample=False, num_beams=1,
|
|
477
|
+
pad_token_id=self.tok.pad_token_id or self.tok.eos_token_id)
|
|
478
|
+
return self.tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True)
|
|
479
|
+
|
|
480
|
+
def nll(self, prefix: str, text: str) -> tuple[float, int]:
|
|
481
|
+
"""Sum of the negative log likelihood of `text`'s tokens after its first
|
|
482
|
+
one, given `prefix` (its own tokens, excluded from the loss), and their count."""
|
|
483
|
+
t = self.torch
|
|
484
|
+
pre = self.tok(prefix, add_special_tokens=False).input_ids if prefix else []
|
|
485
|
+
txt = self.tok(text, add_special_tokens=False).input_ids
|
|
486
|
+
ids = t.tensor([pre + txt], device=self.device)
|
|
487
|
+
with t.no_grad():
|
|
488
|
+
logits = self.model(ids).logits[0, :-1].float()
|
|
489
|
+
logp = t.log_softmax(logits, dim=-1)
|
|
490
|
+
targets = ids[0, 1:]
|
|
491
|
+
tok_nll = -logp.gather(1, targets[:, None])[:, 0]
|
|
492
|
+
start = len(pre) # target index of the text's second token
|
|
493
|
+
sel = tok_nll[start:]
|
|
494
|
+
return float(sel.sum()), int(sel.numel())
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def resolve(name: str) -> str:
|
|
498
|
+
"""The commit sha of `main` on the hub, to be compared with the pinned one."""
|
|
499
|
+
from huggingface_hub import HfApi
|
|
500
|
+
return HfApi().model_info(name).sha
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# ---------------------------------------------------------------------------- #
|
|
504
|
+
# Agreement of two executions, number for number #
|
|
505
|
+
# ---------------------------------------------------------------------------- #
|
|
506
|
+
NUMERIC_KEYS = ("hm_recall_on", "hm_recall_off", "hm_gap", "hm_skill_delta", "ppl_on", "ppl_off", "hm_skill_delta_frame",
|
|
507
|
+
"ppl_frame", "hm_negctrl_rate", "hm_negctrl_abstain_on", "hm_false_abstention_on",
|
|
508
|
+
"hm_invalid_citation_on", "hm_valid_citation_on", "hm_attr_exact_given_valid", "hm_retrieval_hit",
|
|
509
|
+
"hm_guess_rate_off", "hm_malformed_on", "hm_malformed_negctrl", "hm_dissociation_pass", "claimable")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def agree(a: dict, b: dict) -> tuple[bool, list[str]]:
|
|
513
|
+
diffs = [f"{k}: {a.get(k)} != {b.get(k)}" for k in NUMERIC_KEYS if a.get(k) != b.get(k)]
|
|
514
|
+
if a.get("prompt", {}).get("hash") != b.get("prompt", {}).get("hash"):
|
|
515
|
+
diffs.append("prompt hash differs")
|
|
516
|
+
if a.get("model", {}).get("revision") != b.get("model", {}).get("revision"):
|
|
517
|
+
diffs.append("model revision differs")
|
|
518
|
+
return (not diffs), diffs
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ---------------------------------------------------------------------------- #
|
|
522
|
+
# Command line #
|
|
523
|
+
# ---------------------------------------------------------------------------- #
|
|
524
|
+
def _cli(argv=None):
|
|
525
|
+
ap = argparse.ArgumentParser(description="the Molaison LM arm on an external model through the frozen prompt adapter")
|
|
526
|
+
ap.add_argument("--frame", action="store_true", help="write the frozen frame under docs/benchmarks/ and print its hash")
|
|
527
|
+
ap.add_argument("--plant", action="store_true", help="session A: plant the frozen protocol's facts into a sealed journal")
|
|
528
|
+
ap.add_argument("--probe", action="store_true", help="session B, in this new process: run the model, write the file")
|
|
529
|
+
ap.add_argument("--resolve", default=None, help="print the sha of main on the hub for a model id")
|
|
530
|
+
ap.add_argument("--agree", nargs=2, default=None, metavar="FILE", help="compare two files number for number")
|
|
531
|
+
ap.add_argument("--journal", default=None, help="the journal directory (sealed with QUANTUM_CORTEX_JOURNAL_KEY)")
|
|
532
|
+
ap.add_argument("--model", default=None, help="the model id, one of the pinned three unless --revision is given")
|
|
533
|
+
ap.add_argument("--revision", default=None, help="the commit sha (defaults to the pinned one)")
|
|
534
|
+
ap.add_argument("--device", default="cpu")
|
|
535
|
+
ap.add_argument("--raw", action="store_true", help="the frame as raw text (a base model); default: the chat template")
|
|
536
|
+
ap.add_argument("--seed", type=int, default=0)
|
|
537
|
+
ap.add_argument("--n-facts", type=int, default=N_FACTS)
|
|
538
|
+
ap.add_argument("--n-negctrl", type=int, default=N_NEGCTRL)
|
|
539
|
+
ap.add_argument("--n-spans", type=int, default=N_SPANS)
|
|
540
|
+
ap.add_argument("--out", default="metrics/mqar", help="the directory of the artefact")
|
|
541
|
+
ap.add_argument("--repeat", action="store_true", help="name the file -repeat.json (the second execution)")
|
|
542
|
+
a = ap.parse_args(argv)
|
|
543
|
+
|
|
544
|
+
if a.frame:
|
|
545
|
+
frame = build_frame(); p = frame_path(frame); p.parent.mkdir(parents=True, exist_ok=True)
|
|
546
|
+
if p.exists() and p.read_text(encoding="utf-8") != frame:
|
|
547
|
+
raise SystemExit(f"{p} exists with another content: the frame drifted")
|
|
548
|
+
p.write_text(frame, encoding="utf-8", newline="\n") # the same bytes on every machine
|
|
549
|
+
print(f"frame: {p} hash: {frame_hash(frame)} bytes: {len(frame.encode('utf-8'))}")
|
|
550
|
+
return
|
|
551
|
+
if a.resolve:
|
|
552
|
+
sha = resolve(a.resolve)
|
|
553
|
+
pinned = PINNED_MODELS.get(a.resolve)
|
|
554
|
+
print(f"{a.resolve}: main = {sha} pinned = {pinned} {'agrees' if sha == pinned else 'DIFFERS' if pinned else 'not pinned'}")
|
|
555
|
+
return
|
|
556
|
+
if a.agree:
|
|
557
|
+
x, y = (json.loads(Path(f).read_text(encoding="utf-8")) for f in a.agree)
|
|
558
|
+
ok, diffs = agree(x, y)
|
|
559
|
+
print("agree: number for number" if ok else "DISAGREE:\n " + "\n ".join(diffs))
|
|
560
|
+
return
|
|
561
|
+
if a.plant:
|
|
562
|
+
if not a.journal:
|
|
563
|
+
raise SystemExit("--journal is required")
|
|
564
|
+
side = plant(Path(a.journal), n_facts=a.n_facts, seed=a.seed)
|
|
565
|
+
print(json.dumps(side, indent=2))
|
|
566
|
+
return
|
|
567
|
+
if a.probe:
|
|
568
|
+
if not (a.journal and a.model):
|
|
569
|
+
raise SystemExit("--journal and --model are required")
|
|
570
|
+
revision = a.revision or PINNED_MODELS.get(a.model)
|
|
571
|
+
if not revision:
|
|
572
|
+
raise SystemExit(f"{a.model} is not pinned: give --revision (a commit sha), then pin it in the addendum")
|
|
573
|
+
frame = build_frame()
|
|
574
|
+
p = frame_path(frame)
|
|
575
|
+
if not p.exists() or p.read_text(encoding="utf-8") != frame:
|
|
576
|
+
raise SystemExit(f"the frozen frame {p} is missing or differs: run --frame first and commit it")
|
|
577
|
+
model = HFCausalModel(a.model, revision, device=a.device, chat=not a.raw)
|
|
578
|
+
spans, note = load_val_spans(n_spans=a.n_spans, seed=a.seed)
|
|
579
|
+
r = probe(model, Path(a.journal), frame=frame, n_facts=a.n_facts, n_negctrl=a.n_negctrl, seed=a.seed,
|
|
580
|
+
spans=spans, data_note=note)
|
|
581
|
+
out = Path(a.out); out.mkdir(parents=True, exist_ok=True)
|
|
582
|
+
name = f"hm-lm-external-{model_slug(a.model, 'raw' if a.raw else 'chat')}-{frame_hash(frame)}" + ("-repeat" if a.repeat else "") + ".json"
|
|
583
|
+
(out / name).write_text(json.dumps(r, indent=2) + "\n", encoding="utf-8", newline="\n")
|
|
584
|
+
print(f"[record] retained: {out / name}")
|
|
585
|
+
print(json.dumps({k: v for k, v in r.items() if k not in ("answers", "session_a", "skill_arm")}, indent=2))
|
|
586
|
+
return
|
|
587
|
+
ap.print_help()
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
if __name__ == "__main__":
|
|
591
|
+
_cli()
|