muster-memory 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.
muster/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """MUSTER: long-term memory for agents that keeps every word, sets its own strategy from the stored text, and makes no
2
+ model call to build or grow the memory.
3
+
4
+ import muster
5
+ plan = muster.derive(records) # how this store is cut, grouped and read, decided from its own text
6
+ print(muster.explain(plan))
7
+
8
+ Heavy dependencies (numpy, torch, nltk, rank_bm25) are imported only when a step needs them.
9
+ """
10
+ from ._version import __version__
11
+ from .derive import derive, derive_collection, explain
12
+ from .errors import (AssetMissing, EnvironmentRefused, MusterError, PlanError, PlanViolation, RecordError,
13
+ StaleSelection, StoreLocked, StoreRefused)
14
+ from .plan import ENCODER, RERANKER, ModelSpec, Plan
15
+ from .records import DEFAULT_TEMPLATE, Record, numbered, serialize
16
+ from . import presets
17
+ from .memory import Memory, build, context, render, search
18
+ from .models import Models
19
+ from .render import Context, compose
20
+ from .store import AddResult, Store
21
+ from .cache import ScoreCache
22
+ from .source_order import SourceOrder, OrderedRecords, normalize_ordered_records, build_declared
23
+
24
+ __all__ = ["__version__", "build", "search", "render", "context", "compose", "Memory", "Models", "Context", "Store", "AddResult", "ScoreCache", "derive", "derive_collection", "explain", "Plan", "ModelSpec", "ENCODER", "RERANKER",
25
+ "Record", "DEFAULT_TEMPLATE", "numbered", "serialize", "presets", "MusterError", "PlanError", "RecordError",
26
+ "StoreRefused", "EnvironmentRefused", "AssetMissing", "StaleSelection", "PlanViolation", "StoreLocked",
27
+ "SourceOrder", "OrderedRecords", "normalize_ordered_records", "build_declared"]
muster/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
muster/adapters.py ADDED
@@ -0,0 +1,33 @@
1
+ """Turn common conversation shapes into records."""
2
+ from .records import Record
3
+
4
+
5
+ def chat(messages, *, session=None, start=0):
6
+ """[{"role": ..., "content": ..., "time"?: ...}] -> one Record per message, id "S<session>:<n>"."""
7
+ out = []
8
+ for n, m in enumerate(messages, start + 1):
9
+ out.append(Record(text=str(m.get("content", "")), id=("S%s:%d" % (session, n)) if session is not None else None,
10
+ time=m.get("time") or m.get("timestamp"), speaker=m.get("role") or m.get("speaker"),
11
+ session=None if session is None else str(session)))
12
+ return out
13
+
14
+
15
+ def numbered_lines(lines, start=1):
16
+ """A numbered log: "1. <line>", "2. <line>", ... joined by newlines (one record per line)."""
17
+ return "\n".join("%d. %s" % (n, ln) for n, ln in enumerate(lines, start))
18
+
19
+
20
+ def locomo(conversation):
21
+ """LoCoMo's own record line, one per turn: "<dia_id> [<session date_time>] <speaker>: <text>", plus
22
+ " [image: <caption>]" when the turn has an image. Sessions in numeric order."""
23
+ import re
24
+ keys = sorted((k for k in conversation if re.fullmatch(r"session_\d+", k)), key=lambda k: int(k.split("_")[1]))
25
+ out = []
26
+ for k in keys:
27
+ date = conversation[k + "_date_time"]
28
+ for t in conversation[k]:
29
+ text = t["text"]
30
+ if t.get("blip_caption"):
31
+ text += " [image: %s]" % t["blip_caption"]
32
+ out.append(Record(text=text, id=t["dia_id"], time=date, speaker=t["speaker"], session=k.split("_")[1]))
33
+ return out
muster/bpe.py ADDED
@@ -0,0 +1,130 @@
1
+ """Byte-pair encodings.
2
+
3
+ cl100k_base counts tokens for the detection statistics; o200k_base cuts the store and counts unit sizes, as the
4
+ paper's harness does (harness/lme_pf29.py:259, scripts/derive.py:134).
5
+
6
+ The encodings come from tiktoken. If `muster fetch` has put the BPE files in muster's asset directory, they are read
7
+ from there, each checked against the hash tiktoken itself pins, so a run never depends on tiktoken's download cache
8
+ (which lives under the system temp directory). Otherwise tiktoken loads them in its usual way.
9
+ """
10
+ import base64
11
+ import hashlib
12
+ import os
13
+
14
+ from .errors import AssetMissing
15
+
16
+ NAMES = ("cl100k_base", "o200k_base")
17
+ _ENC = {}
18
+
19
+
20
+ def asset_dir():
21
+ """Where `muster fetch` puts tokenizer and sentence-splitter files ($MUSTER_ASSETS, else a per-user data dir)."""
22
+ d = os.environ.get("MUSTER_ASSETS")
23
+ if d:
24
+ return d
25
+ base = os.environ.get("XDG_DATA_HOME") or os.path.join(os.path.expanduser("~"), ".local", "share")
26
+ return os.path.join(base, "muster")
27
+
28
+
29
+ def _bpe_path(name):
30
+ return os.path.join(asset_dir(), "bpe", name + ".tiktoken")
31
+
32
+
33
+ def _constructor(name, ranks_for):
34
+ """Run tiktoken's own constructor for `name` with its file loader replaced by `ranks_for(url, sha256)`.
35
+
36
+ The pattern string and the special tokens therefore come from the installed tiktoken, not from a copy."""
37
+ import tiktoken_ext.openai_public as op
38
+ if name not in op.ENCODING_CONSTRUCTORS:
39
+ raise AssetMissing("tiktoken has no encoding named %r" % name)
40
+ orig = op.load_tiktoken_bpe
41
+ op.load_tiktoken_bpe = lambda url, expected_hash=None: ranks_for(url, expected_hash)
42
+ try:
43
+ return op.ENCODING_CONSTRUCTORS[name]()
44
+ finally:
45
+ op.load_tiktoken_bpe = orig
46
+
47
+
48
+ def source(name):
49
+ """(url, sha256) of the BPE file tiktoken's constructor names."""
50
+ seen = {}
51
+
52
+ def ranks_for(url, sha):
53
+ seen["url"], seen["sha256"] = url, sha
54
+ return {}
55
+ _constructor(name, ranks_for)
56
+ return seen["url"], seen["sha256"]
57
+
58
+
59
+ def _from_file(name, path):
60
+ import tiktoken
61
+ data = open(path, "rb").read()
62
+
63
+ def ranks_for(url, sha):
64
+ if sha and hashlib.sha256(data).hexdigest() != sha:
65
+ raise AssetMissing("%s does not match the sha256 tiktoken pins (%s); run `muster fetch` again" % (path, sha))
66
+ return {base64.b64decode(tok): int(rank) for tok, rank in (ln.split() for ln in data.splitlines() if ln)}
67
+ return tiktoken.Encoding(**_constructor(name, ranks_for))
68
+
69
+
70
+ def encoding(name):
71
+ """The tiktoken Encoding `name`, from muster's asset directory when it is there."""
72
+ enc = _ENC.get(name)
73
+ if enc is None:
74
+ p = _bpe_path(name)
75
+ if os.path.exists(p):
76
+ enc = _from_file(name, p)
77
+ else:
78
+ import tiktoken
79
+ enc = tiktoken.get_encoding(name)
80
+ _ENC[name] = enc
81
+ return enc
82
+
83
+
84
+ def o200k():
85
+ return encoding("o200k_base")
86
+
87
+
88
+ def cl100k():
89
+ return encoding("cl100k_base")
90
+
91
+
92
+ # The three ways the harness encodes. They differ only on text that contains special-token strings.
93
+ def count(enc, text):
94
+ """Token count with special-token strings read as plain text (lme_pf29.py:1127-1128, derive.py:72)."""
95
+ return len(enc.encode(text, disallowed_special=()))
96
+
97
+
98
+ def encode_pack(enc, text):
99
+ """What the packer encodes with (lme_pf29.py:1273): only <|endoftext|> may appear; any other special string raises."""
100
+ return enc.encode(text, allowed_special={"<|endoftext|>"})
101
+
102
+
103
+ def encode_label(enc, text):
104
+ """tiktoken's default mode, used for parent labels (lme_pf29.py:2237): any special-token string raises."""
105
+ return enc.encode(text)
106
+
107
+
108
+ def fetch(names=NAMES, *, quiet=False):
109
+ """Download the BPE files into the asset directory and check each against tiktoken's pinned hash."""
110
+ import urllib.request
111
+ out = []
112
+ for name in names:
113
+ url, sha = source(name)
114
+ dest = _bpe_path(name)
115
+ if os.path.exists(dest) and hashlib.sha256(open(dest, "rb").read()).hexdigest() == sha:
116
+ out.append((name, dest, "present"))
117
+ continue
118
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
119
+ data = urllib.request.urlopen(url, timeout=60).read()
120
+ if hashlib.sha256(data).hexdigest() != sha:
121
+ raise AssetMissing("downloaded %s does not match its pinned sha256" % url)
122
+ tmp = dest + ".tmp"
123
+ with open(tmp, "wb") as f:
124
+ f.write(data)
125
+ os.replace(tmp, dest)
126
+ _ENC.pop(name, None)
127
+ out.append((name, dest, "fetched"))
128
+ if not quiet:
129
+ print("fetched %s -> %s" % (name, dest))
130
+ return out
muster/cache.py ADDED
@@ -0,0 +1,68 @@
1
+ """Text-keyed caches: reranker scores and embedding vectors, in one SQLite file or in memory.
2
+
3
+ A score is keyed by (reranker, query, unit text) and a vector by (encoder, key text), so entries stay valid when a
4
+ store grows and its unit indices move.
5
+ """
6
+ import hashlib
7
+ import sqlite3
8
+ import threading
9
+
10
+
11
+ def _sha(s):
12
+ return hashlib.sha1(s.encode("utf-8", "replace")).digest()
13
+
14
+
15
+ def model_key(obj):
16
+ return "%s@%s" % (getattr(obj, "name", type(obj).__name__), getattr(obj, "revision", "") or "")
17
+
18
+
19
+ class ScoreCache:
20
+ """Reranker scores. `path=None` keeps them in memory."""
21
+
22
+ def __init__(self, path=None, *, model="reranker"):
23
+ self.model = model
24
+ self._lock = threading.Lock()
25
+ self.db = sqlite3.connect(path or ":memory:", check_same_thread=False)
26
+ self.db.execute("CREATE TABLE IF NOT EXISTS scores(model TEXT, qsha BLOB, usha BLOB, score REAL, "
27
+ "PRIMARY KEY (model, qsha, usha))")
28
+ self.db.commit()
29
+
30
+ def get_many(self, query, texts):
31
+ q = _sha(query)
32
+ out = {}
33
+ with self._lock:
34
+ for t in set(texts):
35
+ r = self.db.execute("SELECT score FROM scores WHERE model=? AND qsha=? AND usha=?",
36
+ (self.model, q, _sha(t))).fetchone()
37
+ if r is not None:
38
+ out[t] = r[0]
39
+ return out
40
+
41
+ def put_many(self, query, scores):
42
+ q = _sha(query)
43
+ with self._lock:
44
+ self.db.executemany("INSERT OR REPLACE INTO scores VALUES (?,?,?,?)",
45
+ [(self.model, q, _sha(t), float(v)) for t, v in scores.items()])
46
+ self.db.commit()
47
+
48
+
49
+ class VectorCache:
50
+ """Embedding vectors (float32) keyed by key text."""
51
+
52
+ def __init__(self, db, model, *, create=True):
53
+ self.db, self.model = db, model
54
+ if create:
55
+ self.db.execute("CREATE TABLE IF NOT EXISTS vectors(model TEXT, ksha BLOB, dim INT, vec BLOB, "
56
+ "PRIMARY KEY (model, ksha))")
57
+
58
+ def get_many(self, keys):
59
+ out = {}
60
+ for k in set(keys):
61
+ r = self.db.execute("SELECT vec FROM vectors WHERE model=? AND ksha=?", (self.model, _sha(k))).fetchone()
62
+ if r is not None:
63
+ out[k] = r[0]
64
+ return out
65
+
66
+ def put_many(self, items):
67
+ self.db.executemany("INSERT OR REPLACE INTO vectors VALUES (?,?,?,?)",
68
+ [(self.model, _sha(k), len(v) // 4, v) for k, v in items.items()])
muster/cli.py ADDED
@@ -0,0 +1,248 @@
1
+ """The `muster` command.
2
+
3
+ muster derive PATH [--json] the plan of a store (same output as scripts/derive.py, except a log
4
+ numbered under its own spelling, which the paper's script calls records)
5
+ muster query PATH "question" build a memory from a file once and print what a reader gets
6
+ muster add --store S.muster FILE.jsonl append records (one JSON object per line: text, speaker, time, ...)
7
+ muster context --store S.muster "query" what a reader gets for a query
8
+ muster mcp --store S.muster serve the store to MCP clients over stdio
9
+ muster http --store S.muster [--port N] serve the store as a JSON API
10
+ muster verify cuts --data DIR check the cut of every final-leg row against gate/cut_table.json
11
+ muster verify contexts --data DIR rerun the paper's searches (exact mode) and compare every context hash
12
+ muster presets the paper's presets and the legs each stands for
13
+ muster fetch download the tokenizer and sentence-splitter files once, for offline use
14
+ """
15
+ import argparse
16
+ import json
17
+ import sys
18
+
19
+ from . import bpe
20
+ from .derive import MIN_PARENTS, MIN_UNIT, NUMBERED_TAU, PROSE_STEP, RECORD_STEP, RECORD_TAU
21
+ from .errors import MusterError
22
+ from .io import read_store
23
+ from .stats import LEAD_LINE_TOKENS, lead_serial, store_stats
24
+
25
+ P = 4096
26
+
27
+
28
+ def _decision(serial, record, total, lead=False):
29
+ """The dict scripts/derive.py prints, from the same three numbers (`lead`: numbered under its own spelling)."""
30
+ if serial >= NUMBERED_TAU or lead:
31
+ return dict(store="numbered log", unit_tokens=None, units=None, parent_tokens=None, parents=None,
32
+ order="newest per slot wins", retrieval="mostly by slot",
33
+ channel_floors=dict(meaning=2, keyword=0, parent=8, per_selected_units=10))
34
+ if record >= RECORD_TAU:
35
+ store, unit = "record store", max(P // RECORD_STEP, MIN_UNIT)
36
+ else:
37
+ store, unit = "prose", (P // PROSE_STEP if P > MIN_UNIT else P)
38
+ units = total // unit
39
+ mult = next((m for m in (8, 4, 2) if units // m >= MIN_PARENTS), None)
40
+ return dict(store=store, unit_tokens=unit, units=units, parent_tokens=unit * mult if mult else None,
41
+ parents=units // mult if mult else 0, order="none: all versions kept", retrieval="balanced, 3 channels",
42
+ channel_floors=dict(meaning=4, keyword=3, parent=3, per_selected_units=10))
43
+
44
+
45
+ def _show(path, total, d):
46
+ print("MUSTER %s %s tokens" % (path, format(total, ",")))
47
+ if d["store"] == "numbered log":
48
+ rows = [("store", "numbered log"), ("units", "one per record"), ("parents", "one per slot"),
49
+ ("order", d["order"]), ("retrieval", d["retrieval"])]
50
+ if "leading_serial_share" in d:
51
+ rows.append(("serials", "each line's own prefix, shown as written"))
52
+ else:
53
+ if not d["units"]:
54
+ unit = "whole store (under one unit)"
55
+ elif d["store"] == "record store":
56
+ unit = "512 tokens, records whole"
57
+ else:
58
+ unit = "%s tokens, 4x as many" % format(d["unit_tokens"], ",")
59
+ par = "%s tokens, %d of them" % (format(d["parent_tokens"], ","), d["parents"]) if d["parent_tokens"] else "none (too short)"
60
+ rows = [("store", d["store"]), ("units", unit), ("parents", par), ("order", d["order"]),
61
+ ("retrieval", d["retrieval"])]
62
+ for k, v in rows:
63
+ print(" %-10s %s" % (k, v))
64
+
65
+
66
+ def cmd_derive(a):
67
+ try:
68
+ text = read_store(a.path, a.column, a.row)
69
+ except FileNotFoundError:
70
+ sys.exit("no such file: %s" % a.path)
71
+ except UnicodeDecodeError:
72
+ sys.exit("%s is not UTF-8 text; save it as UTF-8 and run again" % a.path)
73
+ except MusterError as e:
74
+ sys.exit(str(e))
75
+ st = store_stats(text, P)
76
+ if st.tokens == 0:
77
+ sys.exit("%s is empty: nothing to configure" % a.path)
78
+ share, med = lead_serial(text) if st.serial < NUMBERED_TAU else (0.0, None)
79
+ lead = med is not None and med <= LEAD_LINE_TOKENS
80
+ d = _decision(st.serial, st.record_share, st.tokens, lead=lead)
81
+ if lead:
82
+ d = dict(leading_serial_share=round(share, 4), median_line_tokens=med, **d)
83
+ if a.json:
84
+ print(json.dumps(dict(path=a.path, tokens=st.tokens, serial_fraction=round(st.serial, 4),
85
+ record_share=round(st.record_share, 4), **d), indent=1))
86
+ else:
87
+ _show(a.path, st.tokens, d)
88
+
89
+
90
+ def cmd_verify(a):
91
+ import os
92
+ from .compat.verify import verify_cuts
93
+ root = a.repo or os.getcwd()
94
+ legs = set(a.legs.split(",")) if a.legs else None
95
+ if a.what == "cuts":
96
+ ok = verify_cuts(a.data, os.path.join(root, "gate", "cut_table.json"), legs=legs)
97
+ else:
98
+ from .compat.verify import verify_contexts
99
+ ok = verify_contexts(a.data, os.path.join(root, "gate", "ctx_table.jsonl"),
100
+ os.path.join(root, "gate", "cut_table.json"), legs=legs,
101
+ cache_dirs=[d for d in (a.cache or "").split(":") if d], nq=a.nq or None)
102
+ sys.exit(0 if ok else 1)
103
+
104
+
105
+ def cmd_presets(a):
106
+ from . import presets
107
+ by = {}
108
+ for tag, leg in presets.legs().items():
109
+ by.setdefault(leg["preset"], []).append(tag)
110
+ for name in presets.names():
111
+ p = presets.get(name)
112
+ shape = ("%s/%s/%s" % (p.atom, p.chunk, p.mscale)) if p.atom else p.branch
113
+ print("%-11s %-9s %-16s U=%-3d %-13s %-8s %s" % (name, p.branch, shape, p.U, p.policy, p.render,
114
+ ", ".join(sorted(by.get(name, [])))))
115
+
116
+
117
+ def cmd_fetch(a):
118
+ for name, dest, what in bpe.fetch(quiet=True):
119
+ print("%-12s %-8s %s" % (name, what, dest))
120
+ from .cut import _ensure_punkt
121
+ _ensure_punkt(exact=False)
122
+ print("punkt_tab ready")
123
+ if a.models:
124
+ from huggingface_hub import snapshot_download
125
+ from .plan import ENCODER, RERANKER
126
+ for spec in (ENCODER, RERANKER):
127
+ path = snapshot_download(spec.name, revision=spec.revision)
128
+ print("%-12s ready %s" % (spec.name.split("/")[-1], path))
129
+
130
+
131
+ def _models(a):
132
+ if getattr(a, "fake_models", False):
133
+ from .models import Models
134
+ return Models.fake(32)
135
+ return None
136
+
137
+
138
+ def cmd_add(a):
139
+ from .store import Store
140
+ recs = [json.loads(x) for x in open(a.file, encoding="utf-8") if x.strip()] if a.file != "-" else \
141
+ [json.loads(x) for x in sys.stdin if x.strip()]
142
+ with Store.open(a.store, models=_models(a)) as st:
143
+ r = st.add(recs, request_id=a.request_id)
144
+ print(json.dumps({"version": r.version, "duplicate": r.duplicate, "n_records": r.n_records}))
145
+
146
+
147
+ def cmd_context(a):
148
+ from .store import Store
149
+ with Store.open(a.store, models=_models(a), readonly=True) as st:
150
+ ctx = st.context(a.query, U=a.units or None)
151
+ print(ctx.text)
152
+
153
+
154
+ def cmd_query(a):
155
+ from .memory import build
156
+ from .records import as_record
157
+ if a.declared_records:
158
+ from .source_order import build_declared
159
+ with open(a.path, encoding="utf-8") as source:
160
+ envelope = json.load(source)
161
+ mem = build_declared(envelope, models=_models(a))
162
+ elif a.path.endswith(".jsonl") and not a.column_given:
163
+ store = [as_record(json.loads(x)) for x in open(a.path, encoding="utf-8") if x.strip()]
164
+ mem = build(store, models=_models(a))
165
+ else:
166
+ store = read_store(a.path, a.column, a.row)
167
+ mem = build(store, models=_models(a))
168
+ if a.explain:
169
+ from .derive import explain
170
+ print(explain(mem.plan), file=sys.stderr)
171
+ print(mem.context(a.question, U=a.units or None).text)
172
+
173
+
174
+ def cmd_mcp(a):
175
+ from .serve import Service, mcp_server
176
+ from .store import Store
177
+ mcp_server(Service(Store.open(a.store, models=_models(a)))).run(transport=a.transport)
178
+
179
+
180
+ def cmd_http(a):
181
+ from .serve import Service, http_server
182
+ from .store import Store
183
+ srv = http_server(Service(Store.open(a.store, models=_models(a)),
184
+ allow_declared_records=a.allow_declared_records), a.host, a.port)
185
+ print("muster http on %s:%d (POST /v1/add /v1/context /v1/search, GET /v1/info)" % (a.host, a.port), flush=True)
186
+ srv.serve_forever()
187
+
188
+
189
+ def main(argv=None):
190
+ ap = argparse.ArgumentParser(prog="muster", description="MUSTER memory: derive, presets, fetch")
191
+ sub = ap.add_subparsers(dest="cmd", required=True)
192
+ d = sub.add_parser("derive", help="the plan of a store, from its own text")
193
+ d.add_argument("path")
194
+ d.add_argument("--column", default="context")
195
+ d.add_argument("--row", type=int, default=0)
196
+ d.add_argument("--json", action="store_true")
197
+ d.set_defaults(fn=cmd_derive)
198
+ q = sub.add_parser("query", help="build a memory from a file and print the context for a question")
199
+ q.add_argument("path")
200
+ q.add_argument("question")
201
+ q.add_argument("--units", type=int, default=0)
202
+ q.add_argument("--column", default=None)
203
+ q.add_argument("--row", type=int, default=0)
204
+ q.add_argument("--explain", action="store_true", help="print the plan and why, to stderr")
205
+ q.add_argument("--declared-records", action="store_true",
206
+ help="native opt-in: JSON envelope with explicit record boundaries and source-order provenance")
207
+ q.add_argument("--fake-models", action="store_true", help=argparse.SUPPRESS)
208
+ q.set_defaults(fn=cmd_query)
209
+ for name, fn, extra in (("add", cmd_add, ("file",)), ("context", cmd_context, ("query",)),
210
+ ("mcp", cmd_mcp, ()), ("http", cmd_http, ())):
211
+ p = sub.add_parser(name)
212
+ p.add_argument("--store", required=True)
213
+ p.add_argument("--fake-models", action="store_true", help=argparse.SUPPRESS)
214
+ for x in extra:
215
+ p.add_argument(x)
216
+ if name == "add":
217
+ p.add_argument("--request-id", default=None)
218
+ if name == "context":
219
+ p.add_argument("--units", type=int, default=0)
220
+ if name == "mcp":
221
+ p.add_argument("--transport", default="stdio", choices=["stdio", "streamable-http"])
222
+ if name == "http":
223
+ p.add_argument("--host", default="127.0.0.1")
224
+ p.add_argument("--port", type=int, default=8765)
225
+ p.add_argument("--allow-declared-records", action="store_true",
226
+ help="enable stateless /v1/declared-context for explicit source-order envelopes")
227
+ p.set_defaults(fn=fn)
228
+ v = sub.add_parser("verify", help="check the library against the paper's tables (no benchmark text needed)")
229
+ v.add_argument("what", choices=["cuts", "contexts"])
230
+ v.add_argument("--cache", default="", help="contexts: colon-separated directories of published reranker caches")
231
+ v.add_argument("--nq", type=int, default=0, help="contexts: at most this many questions per store")
232
+ v.add_argument("--data", required=True, help="directory with the benchmark files at the legs' paths")
233
+ v.add_argument("--repo", default=None, help="repository root holding gate/ (default: the current directory)")
234
+ v.add_argument("--legs", default="", help="comma-separated leg tags (default: all)")
235
+ v.set_defaults(fn=cmd_verify)
236
+ sub.add_parser("presets", help="the paper's presets").set_defaults(fn=cmd_presets)
237
+ f = sub.add_parser("fetch", help="download tokenizer and sentence-splitter files (and, with --models, both models)")
238
+ f.add_argument("--models", action="store_true", help="also download bge-m3 and bge-reranker-v2-m3 at the pinned revisions")
239
+ f.set_defaults(fn=cmd_fetch)
240
+ a = ap.parse_args(argv)
241
+ if a.cmd == "query":
242
+ a.column_given = a.column is not None
243
+ a.column = a.column or "context"
244
+ a.fn(a)
245
+
246
+
247
+ if __name__ == "__main__":
248
+ main()
@@ -0,0 +1 @@
1
+ """Reproduction tools: the paper's legs, its reranker-cache files, its bundles. Not needed to use MUSTER."""
@@ -0,0 +1,145 @@
1
+ """Bundles: one JSON line per question with the context a published read consumed.
2
+
3
+ Library versions of harness/make_mab_bundle.py, harness/make_bundle.py, harness/render_rule.py (R4) and the
4
+ FactConsolidation tag strip of gate/run_mab_ctx.sh. They write the same bytes, so a regenerated file can be compared
5
+ with a published one by its sha256.
6
+ """
7
+ import collections
8
+ import glob
9
+ import hashlib
10
+ import json
11
+ import os
12
+
13
+ from .recover import index80, recover_ids
14
+ from ..render import per_parent_groups
15
+
16
+
17
+ def _h(s):
18
+ return hashlib.sha1(s.encode()).hexdigest()[:12]
19
+
20
+
21
+ def _gold(a):
22
+ return [str(x) for x in a] if hasattr(a, "__len__") and not isinstance(a, str) else [str(a)]
23
+
24
+
25
+ def read_dump(path, *, on_duplicate="error"):
26
+ """{(row, qi): record} of a DRY dump. A repeated (row, qi) is an error unless on_duplicate="last"."""
27
+ recs, dup = {}, 0
28
+ for line in open(path):
29
+ if not line.strip():
30
+ continue
31
+ r = json.loads(line)
32
+ k = (r["row"], r["qi"])
33
+ dup += k in recs
34
+ recs[k] = r
35
+ if dup and on_duplicate == "error":
36
+ raise ValueError("%s: %d duplicated (row, qi)" % (path, dup))
37
+ return recs, dup
38
+
39
+
40
+ def bundle_mab(dump_path, *, tag, arm, df, nq, out, dsname, view="prune_annot"):
41
+ """make_mab_bundle.py: the scored arm's context per question, checked against its recorded hash."""
42
+ V = arm + "|" + view
43
+ recs, _ = read_dump(dump_path)
44
+ if len(recs) != nq:
45
+ raise ValueError("%s: partial export %d/%d" % (tag, len(recs), nq))
46
+ n = 0
47
+ with open(out, "w") as fo:
48
+ for k in sorted(recs):
49
+ r = recs[k]
50
+ row, qi = k
51
+ ctx = r["ctx|" + V]
52
+ h = _h(ctx)
53
+ if h != r["ctxhash|" + V]:
54
+ raise ValueError("%s %s: context does not hash to its recorded ctxhash" % (tag, k))
55
+ m = df["metadata"].iloc[row]
56
+ qs = list(df["questions"].iloc[row])
57
+ ans = df["answers"].iloc[row]
58
+ ids = list(m["question_ids"]) if m.get("question_ids") is not None else []
59
+ lines = [x for x in ctx.split("\n") if x.strip()]
60
+ c = ctx.count("[CURRENT]")
61
+ fo.write(json.dumps(dict(row=row, qi=qi, qt=r["qt"], dataset=dsname, question=str(qs[qi]), gold=_gold(ans[qi]),
62
+ gold_str=r["gold"], question_id=str(ids[qi]) if qi < len(ids) else "", arm=V,
63
+ src_tag=tag, ctxhash=h, tok=(r.get("tok_by_arm") or {}).get(V), n_lines=len(lines),
64
+ n_current=c, n_units=r.get("n_units"), context=ctx), ensure_ascii=False) + "\n")
65
+ n += 1
66
+ return n
67
+
68
+
69
+ def bundle_beam(dump_paths, *, df, out, arm="flr_4_3_3|prune_annot"):
70
+ """make_bundle.py: every question of every conversation, in parquet order. Later shards win a repeated key,
71
+ as the original did; the repeats are returned so a caller can report them."""
72
+ recs, dups = {}, []
73
+ for f in dump_paths:
74
+ for line in open(f):
75
+ r = json.loads(line)
76
+ k = (r["row"], r["qi"])
77
+ if k in recs:
78
+ dups.append((k, recs[k]["ctxhash|" + arm] == r["ctxhash|" + arm]))
79
+ recs[k] = r
80
+ nq = sum(len(df.iloc[i]["questions"]) for i in range(len(df)))
81
+ if len(recs) != nq:
82
+ raise ValueError("partial export: %d/%d" % (len(recs), nq))
83
+ n = 0
84
+ with open(out, "w") as fo:
85
+ for row in range(len(df)):
86
+ m = dict(df.iloc[row]["metadata"])
87
+ qs = list(df.iloc[row]["questions"])
88
+ ans = df.iloc[row]["answers"]
89
+ for qi in range(len(qs)):
90
+ r = recs[(row, qi)]
91
+ ctx = r["ctx|" + arm]
92
+ if _h(ctx) != r["ctxhash|" + arm]:
93
+ raise ValueError("%s: context does not hash to its recorded ctxhash" % ((row, qi),))
94
+ fo.write(json.dumps(dict(row=row, qi=qi, conv_id=str(m["conv_id"]), category=str(m["question_types"][qi]),
95
+ cat_index=int(m["cat_index"][qi]), question=str(qs[qi]), gold=_gold(ans[qi]),
96
+ rubric=[str(x) for x in m["rubric"][qi]], ctxhash=r["ctxhash|" + arm],
97
+ n_units=r.get("n_units"), tok=(r.get("tok_by_arm") or {}).get(arm),
98
+ context=ctx), ensure_ascii=False) + "\n")
99
+ n += 1
100
+ return n, dups
101
+
102
+
103
+ def r4_bundle(bundle_in, out, units_of_row):
104
+ """render_rule.py R4: recover the ids each context holds, render one block per parent. `units_of_row(row)` returns
105
+ (units, parent) of that row's store under the leg's cut."""
106
+ stat = collections.Counter()
107
+ cache = {}
108
+ with open(out, "w") as fo:
109
+ for line in open(bundle_in):
110
+ r = json.loads(line)
111
+ row = r["row"]
112
+ if row not in cache:
113
+ u, p = units_of_row(row)
114
+ cache[row] = (u, p, index80(u))
115
+ units, parent, idx = cache[row]
116
+ ids = recover_ids(r["context"], units, parent, idx)
117
+ groups = per_parent_groups(ids, parent)
118
+ new = "\n".join("Memory %d:\n%s" % (n + 1, "\n".join(units[i] for i in b)) for n, b in enumerate(groups))
119
+ stat["same_as_input"] += new == r["context"]
120
+ r["context"] = new
121
+ r["ctxhash"] = _h(new)
122
+ fo.write(json.dumps(r, ensure_ascii=False) + "\n")
123
+ stat["n"] += 1
124
+ return dict(stat)
125
+
126
+
127
+ def notag(bundle_in, out):
128
+ """FactConsolidation: drop the "[CURRENT] " tag the structural render writes before the newest record of a slot."""
129
+ rows = [json.loads(line) for line in open(bundle_in)]
130
+ n_tag = 0
131
+ for d in rows:
132
+ n_tag += d["context"].count("[CURRENT] ")
133
+ d["context"] = d["context"].replace("[CURRENT] ", "")
134
+ if "[CURRENT]" in d["context"]:
135
+ raise ValueError("a [CURRENT] tag without its trailing space survived the strip")
136
+ d["ctxhash"] = _h(d["context"])
137
+ with open(out, "w") as fo:
138
+ for d in rows:
139
+ fo.write(json.dumps(d, ensure_ascii=False) + "\n")
140
+ return len(rows), n_tag
141
+
142
+
143
+ def shards(results_dir, tag):
144
+ return sorted(glob.glob(os.path.join(results_dir, "%s_s*.jsonl" % tag)) or
145
+ glob.glob(os.path.join(results_dir, "%s_r*.jsonl" % tag)))