cortexm 0.3.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.
- context_m.py +17 -0
- cortexm/__init__.py +45 -0
- cortexm/accel.py +403 -0
- cortexm/api/__init__.py +0 -0
- cortexm/api/chaos.py +118 -0
- cortexm/api/memory.py +635 -0
- cortexm/bench/__init__.py +0 -0
- cortexm/bench/abilities.py +311 -0
- cortexm/bench/baselines.py +89 -0
- cortexm/bench/beam_loader.py +317 -0
- cortexm/bench/generator.py +376 -0
- cortexm/bench/harness.py +211 -0
- cortexm/bench/messy.py +218 -0
- cortexm/bench/micro.py +251 -0
- cortexm/bench/ood.py +443 -0
- cortexm/bench/run.py +137 -0
- cortexm/bridge/__init__.py +0 -0
- cortexm/bridge/dates.py +178 -0
- cortexm/bridge/decoders.py +204 -0
- cortexm/bridge/enrich.py +255 -0
- cortexm/bridge/extractor.py +316 -0
- cortexm/bridge/fallback.py +332 -0
- cortexm/bridge/onnx_runtime.py +158 -0
- cortexm/bridge/patterns.py +760 -0
- cortexm/bridge/ppr.py +104 -0
- cortexm/bridge/prefilter.py +188 -0
- cortexm/bridge/query_extract.py +420 -0
- cortexm/bridge/reader.py +1174 -0
- cortexm/bridge/rerank.py +204 -0
- cortexm/bridge/writer.py +492 -0
- cortexm/cli.py +295 -0
- cortexm/cognition/__init__.py +53 -0
- cortexm/cognition/abstraction.py +192 -0
- cortexm/cognition/analogy.py +159 -0
- cortexm/cognition/engine.py +204 -0
- cortexm/cognition/gaps.py +365 -0
- cortexm/cognition/scanner.py +204 -0
- cortexm/config.py +375 -0
- cortexm/cortexm.py +8 -0
- cortexm/enterprise/__init__.py +0 -0
- cortexm/enterprise/audit.py +178 -0
- cortexm/enterprise/governance.py +239 -0
- cortexm/errors.py +35 -0
- cortexm/features/__init__.py +0 -0
- cortexm/features/git.py +204 -0
- cortexm/features/prefetch.py +88 -0
- cortexm/features/zk.py +105 -0
- cortexm/federation/__init__.py +39 -0
- cortexm/federation/crdt.py +275 -0
- cortexm/federation/fabric.py +109 -0
- cortexm/federation/hlc.py +80 -0
- cortexm/federation/node.py +145 -0
- cortexm/federation/schema_report.py +73 -0
- cortexm/federation/transport.py +164 -0
- cortexm/index/__init__.py +19 -0
- cortexm/index/nsg.py +386 -0
- cortexm/mcp/__init__.py +0 -0
- cortexm/mcp/server.py +985 -0
- cortexm/metrics.py +62 -0
- cortexm/migrate/__init__.py +0 -0
- cortexm/migrate/importers.py +192 -0
- cortexm/provenance/__init__.py +78 -0
- cortexm/provenance/agent.py +214 -0
- cortexm/provenance/cose.py +201 -0
- cortexm/provenance/scitt.py +258 -0
- cortexm/provenance/vc.py +250 -0
- cortexm/security/__init__.py +0 -0
- cortexm/security/crypto.py +162 -0
- cortexm/security/hashes.py +140 -0
- cortexm/security/injection.py +149 -0
- cortexm/security/mind.py +154 -0
- cortexm/security/pii.py +265 -0
- cortexm/security/rbac.py +169 -0
- cortexm/security/sandbox.py +131 -0
- cortexm/security/zk_hamming.py +142 -0
- cortexm/security/zk_sql.py +485 -0
- cortexm/server/__init__.py +0 -0
- cortexm/server/metrics.py +88 -0
- cortexm/server/rest.py +936 -0
- cortexm/server/sparql.py +984 -0
- cortexm/text/__init__.py +0 -0
- cortexm/text/dissim.py +252 -0
- cortexm/text/embedder.py +155 -0
- cortexm/text/fuzzy.py +218 -0
- cortexm/text/idiolect.py +253 -0
- cortexm/text/labse.py +374 -0
- cortexm/text/tokenizer.py +79 -0
- cortexm/trace/__init__.py +0 -0
- cortexm/trace/blob_arena.py +277 -0
- cortexm/trace/consolidate.py +337 -0
- cortexm/trace/contradictions.py +69 -0
- cortexm/trace/dedup.py +114 -0
- cortexm/trace/edges.py +214 -0
- cortexm/trace/fact.py +121 -0
- cortexm/trace/fade.py +245 -0
- cortexm/trace/lifecycle.py +112 -0
- cortexm/trace/rebuild.py +173 -0
- cortexm/trace/rules.py +171 -0
- cortexm/trace/store.py +680 -0
- cortexm/trace/structural.py +183 -0
- cortexm/trace/tmt.py +335 -0
- cortexm/util.py +148 -0
- cortexm/vsa/__init__.py +0 -0
- cortexm/vsa/attribution.py +149 -0
- cortexm/vsa/cleanup.py +161 -0
- cortexm/vsa/codecs.py +397 -0
- cortexm/vsa/hologram_overlay.py +139 -0
- cortexm/vsa/index.py +163 -0
- cortexm/vsa/ops.py +149 -0
- cortexm/vsa/palace.py +446 -0
- cortexm/vsa/role_vectors.py +236 -0
- cortexm/vsa/slb.py +78 -0
- cortexm/vsa/tlsh_trie.py +137 -0
- cortexm/vsa/working_memory.py +249 -0
- cortexm-0.3.0.dist-info/METADATA +482 -0
- cortexm-0.3.0.dist-info/RECORD +120 -0
- cortexm-0.3.0.dist-info/WHEEL +5 -0
- cortexm-0.3.0.dist-info/entry_points.txt +2 -0
- cortexm-0.3.0.dist-info/licenses/LICENSE +190 -0
- cortexm-0.3.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""BEAM-10M dataset loader — fetches real conversations from HF.
|
|
2
|
+
|
|
3
|
+
BEAM-10M (Mohammadta/BEAM-10M) is a LongMemEval-style benchmark:
|
|
4
|
+
each row is a long, multi-session conversation with:
|
|
5
|
+
- conversation_id
|
|
6
|
+
- user_profile (user_info + user_relationships) — ground-truth facts
|
|
7
|
+
- narratives — a structured timeline of events
|
|
8
|
+
- chat — 10 plans, each with 10 batches of ~60 turns
|
|
9
|
+
- probing_questions — questions to test memory of the conversation
|
|
10
|
+
- plans — alternate plans structure
|
|
11
|
+
|
|
12
|
+
The HuggingFace datasets API is rate-limited from many sandboxes
|
|
13
|
+
(429 Too Many Requests from CloudFront). The datasets-server endpoint
|
|
14
|
+
(hosted separately) IS reachable. This loader pulls via datasets-server
|
|
15
|
+
so the bench works even when direct HF datasets API is blocked.
|
|
16
|
+
|
|
17
|
+
BULK DOWNLOAD PATHS (added for full-dataset benchmarks):
|
|
18
|
+
- Local cache directory of per-row JSON files (preferred — produced
|
|
19
|
+
by scripts/download_beam_full.sh OR by the .github/workflows/
|
|
20
|
+
beam-cache.yml workflow that runs on a GitHub Actions runner)
|
|
21
|
+
- Local parquet file (if you have downloaded the full parquet via
|
|
22
|
+
`huggingface-cli download Mohammadta/BEAM-10M` on a runner whose
|
|
23
|
+
IP is not rate-limited; the file is ~342MB). Pass the path via
|
|
24
|
+
the BEAM_PARQUET env var or the parquet_path argument.
|
|
25
|
+
- datasets-server /rows endpoint as fallback (works in sandboxes)
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
from cortexm.bench.beam_loader import load_beam_rows
|
|
29
|
+
rows = load_beam_rows(n=10) # fetch all 10 conversations
|
|
30
|
+
for r in rows:
|
|
31
|
+
print(r['conversation_id'])
|
|
32
|
+
print(r['user_profile']['user_info'])
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import urllib.request
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Iterator
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
DATASETS_SERVER = "https://datasets-server.huggingface.co"
|
|
44
|
+
DATASET_NAME = "Mohammadta/BEAM-10M"
|
|
45
|
+
CONFIG = "default"
|
|
46
|
+
SPLIT = "10M"
|
|
47
|
+
TOTAL_ROWS = 10 # the 10M split has 10 conversations, each ~10M tokens
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fetch_rows(offset: int = 0, length: int = 1) -> dict:
|
|
51
|
+
"""Fetch BEAM-10M rows via the datasets-server endpoint."""
|
|
52
|
+
url = (f"{DATASETS_SERVER}/rows?dataset={DATASET_NAME}"
|
|
53
|
+
f"&config={CONFIG}&split={SPLIT}"
|
|
54
|
+
f"&offset={offset}&length={length}")
|
|
55
|
+
req = urllib.request.Request(
|
|
56
|
+
url, headers={"User-Agent": "context-m-bench/1.0"})
|
|
57
|
+
with urllib.request.urlopen(req, timeout=600) as resp:
|
|
58
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_parquet(parquet_path: str | Path, n: int = TOTAL_ROWS) -> list[dict]:
|
|
62
|
+
"""Load rows from a local parquet file (requires pyarrow or pandas).
|
|
63
|
+
|
|
64
|
+
Each parquet row has the same shape as the datasets-server /rows
|
|
65
|
+
endpoint's `row` field. We return just the row dicts (no envelope).
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
import pyarrow.parquet as pq
|
|
69
|
+
except ImportError:
|
|
70
|
+
try:
|
|
71
|
+
import pandas as pd
|
|
72
|
+
except ImportError:
|
|
73
|
+
raise ImportError(
|
|
74
|
+
"loading BEAM-10M from parquet requires pyarrow or "
|
|
75
|
+
"pandas — install with `pip install pyarrow` or "
|
|
76
|
+
"`pip install pandas`")
|
|
77
|
+
df = pd.read_parquet(parquet_path)
|
|
78
|
+
rows = []
|
|
79
|
+
for _, r in df.head(n).iterrows():
|
|
80
|
+
rows.append(r.to_dict() if hasattr(r, "to_dict") else dict(r))
|
|
81
|
+
return rows
|
|
82
|
+
table = pq.read_table(parquet_path)
|
|
83
|
+
n = min(n, table.num_rows)
|
|
84
|
+
rows = []
|
|
85
|
+
# batch-convert to Python dicts (column-at-a-time is faster than row)
|
|
86
|
+
cols = {name: table.column(name).to_pylist() for name in table.column_names}
|
|
87
|
+
for i in range(n):
|
|
88
|
+
rows.append({name: cols[name][i] for name in cols})
|
|
89
|
+
return rows
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_beam_rows(n: int = 2, *, cache_dir: str | None = None,
|
|
93
|
+
parquet_path: str | None = None) -> list[dict]:
|
|
94
|
+
"""Load N BEAM-10M conversations.
|
|
95
|
+
|
|
96
|
+
Resolution order (first available wins):
|
|
97
|
+
1. parquet_path argument (or BEAM_PARQUET env var) — a single
|
|
98
|
+
parquet file containing all 10 rows
|
|
99
|
+
2. cache_dir/beam_row_<i>.json — per-row JSON files (the format
|
|
100
|
+
produced by download_beam_full.sh and beam-cache.yml)
|
|
101
|
+
3. datasets-server /rows endpoint — streamed on demand
|
|
102
|
+
|
|
103
|
+
Each row is a dict with keys: conversation_id, conversation_seed,
|
|
104
|
+
narratives, user_profile, conversation_plan, user_questions, chat,
|
|
105
|
+
probing_questions, plans.
|
|
106
|
+
|
|
107
|
+
The full 10M dataset has 10 conversations totaling ~975MB in
|
|
108
|
+
memory. We fetch one row at a time (each is ~50-110MB).
|
|
109
|
+
"""
|
|
110
|
+
# 1. parquet path?
|
|
111
|
+
pq_path = parquet_path or os.environ.get("BEAM_PARQUET")
|
|
112
|
+
if pq_path and Path(pq_path).exists():
|
|
113
|
+
return _load_parquet(pq_path, n=min(n, TOTAL_ROWS))
|
|
114
|
+
|
|
115
|
+
cache = Path(cache_dir) if cache_dir else None
|
|
116
|
+
if cache:
|
|
117
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
rows: list[dict] = []
|
|
120
|
+
n = min(n, TOTAL_ROWS)
|
|
121
|
+
for i in range(n):
|
|
122
|
+
# 2. cached row?
|
|
123
|
+
cached_path = cache / f"beam_row_{i}.json" if cache else None
|
|
124
|
+
if cached_path and cached_path.exists():
|
|
125
|
+
try:
|
|
126
|
+
data = json.loads(cached_path.read_text())
|
|
127
|
+
if "rows" in data and data["rows"]:
|
|
128
|
+
rows.append(data["rows"][0]["row"])
|
|
129
|
+
continue
|
|
130
|
+
# some manifests store the row directly without envelope
|
|
131
|
+
if "conversation_id" in data:
|
|
132
|
+
rows.append(data)
|
|
133
|
+
continue
|
|
134
|
+
except (json.JSONDecodeError, KeyError):
|
|
135
|
+
# corrupt cache — re-fetch
|
|
136
|
+
cached_path.unlink(missing_ok=True)
|
|
137
|
+
# 3. fetch from datasets-server
|
|
138
|
+
data = _fetch_rows(offset=i, length=1)
|
|
139
|
+
if cached_path:
|
|
140
|
+
cached_path.write_text(json.dumps(data))
|
|
141
|
+
if "rows" in data and data["rows"]:
|
|
142
|
+
rows.append(data["rows"][0]["row"])
|
|
143
|
+
return rows
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_user_facts(row: dict) -> list[dict]:
|
|
147
|
+
"""Extract ground-truth facts from a BEAM row's user_profile.
|
|
148
|
+
|
|
149
|
+
The user_profile has:
|
|
150
|
+
user_info: free text "Name: Jennifer Mccall / Age: 59 / ..."
|
|
151
|
+
user_relationships: free text with bullet points
|
|
152
|
+
|
|
153
|
+
We extract structured facts via regex (zero LLM calls).
|
|
154
|
+
"""
|
|
155
|
+
import re
|
|
156
|
+
up = row.get("user_profile", {})
|
|
157
|
+
info = up.get("user_info", "") or ""
|
|
158
|
+
rels = up.get("user_relationships", "") or ""
|
|
159
|
+
|
|
160
|
+
facts: list[dict] = []
|
|
161
|
+
conv_id = row.get("conversation_id", "unknown")
|
|
162
|
+
user_id = f"beam_{conv_id}"
|
|
163
|
+
|
|
164
|
+
# name
|
|
165
|
+
m = re.search(r"Name:\s*([^\n]+)", info)
|
|
166
|
+
if m:
|
|
167
|
+
facts.append({"subject": user_id, "relation": "name",
|
|
168
|
+
"value": m.group(1).strip()})
|
|
169
|
+
# age
|
|
170
|
+
m = re.search(r"Age:\s*(\d+)", info)
|
|
171
|
+
if m:
|
|
172
|
+
facts.append({"subject": user_id, "relation": "age",
|
|
173
|
+
"value": m.group(1).strip()})
|
|
174
|
+
# gender
|
|
175
|
+
m = re.search(r"Gender:\s*([^\n]+)", info)
|
|
176
|
+
if m:
|
|
177
|
+
facts.append({"subject": user_id, "relation": "gender",
|
|
178
|
+
"value": m.group(1).strip()})
|
|
179
|
+
# location
|
|
180
|
+
m = re.search(r"Location:\s*([^\n]+)", info)
|
|
181
|
+
if m:
|
|
182
|
+
facts.append({"subject": user_id, "relation": "location",
|
|
183
|
+
"value": m.group(1).strip()})
|
|
184
|
+
# profession
|
|
185
|
+
m = re.search(r"Profession:\s*([^\n]+)", info)
|
|
186
|
+
if m:
|
|
187
|
+
facts.append({"subject": user_id, "relation": "profession",
|
|
188
|
+
"value": m.group(1).strip()})
|
|
189
|
+
|
|
190
|
+
# relationships — extract names + relations
|
|
191
|
+
# relationship sections start with all-caps headers like
|
|
192
|
+
# "PARENTS & GUARDIANS:" "ROMANTIC PARTNER:" "CHILDREN:" etc.
|
|
193
|
+
cur_section = None
|
|
194
|
+
for line in rels.split("\n"):
|
|
195
|
+
line = line.strip()
|
|
196
|
+
if not line:
|
|
197
|
+
continue
|
|
198
|
+
# detect header — line is all caps + colon
|
|
199
|
+
if line.endswith(":") and line[:-1] == line[:-1].upper():
|
|
200
|
+
cur_section = line[:-1]
|
|
201
|
+
continue
|
|
202
|
+
# bullet line — extract name + age
|
|
203
|
+
m = re.match(r"•\s*([^\(]+)\s*\(.*?\)", line)
|
|
204
|
+
if m and cur_section:
|
|
205
|
+
name = m.group(1).strip().rstrip(",")
|
|
206
|
+
# convert section to relation
|
|
207
|
+
rel_map = {
|
|
208
|
+
"PARENTS & GUARDIANS": "parent",
|
|
209
|
+
"ROMANTIC PARTNER": "partner",
|
|
210
|
+
"CHILDREN": "child",
|
|
211
|
+
"SIBLINGS": "sibling",
|
|
212
|
+
"FRIENDS": "friend",
|
|
213
|
+
"COLLEAGUES": "colleague",
|
|
214
|
+
}
|
|
215
|
+
relation = rel_map.get(cur_section, cur_section.lower())
|
|
216
|
+
facts.append({"subject": user_id, "relation": relation,
|
|
217
|
+
"value": name})
|
|
218
|
+
return facts
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_chat_turns(row: dict) -> list[dict]:
|
|
222
|
+
"""Flatten a row's chat history into a list of user turns.
|
|
223
|
+
|
|
224
|
+
Each turn has: content, id, index, question_type, role, time_anchor.
|
|
225
|
+
We only keep role='user' turns (these are the messages the user
|
|
226
|
+
actually said — what Context-M needs to extract facts from).
|
|
227
|
+
"""
|
|
228
|
+
chat = row.get("chat", [])
|
|
229
|
+
if not isinstance(chat, list):
|
|
230
|
+
return []
|
|
231
|
+
turns_out: list[dict] = []
|
|
232
|
+
for plan_obj in chat:
|
|
233
|
+
if not isinstance(plan_obj, dict):
|
|
234
|
+
continue
|
|
235
|
+
# plan_obj has one key like "plan-1"
|
|
236
|
+
for plan_name, batches in plan_obj.items():
|
|
237
|
+
if not isinstance(batches, list):
|
|
238
|
+
continue
|
|
239
|
+
for batch in batches:
|
|
240
|
+
if not isinstance(batch, dict):
|
|
241
|
+
continue
|
|
242
|
+
turns = batch.get("turns", [])
|
|
243
|
+
if not isinstance(turns, list):
|
|
244
|
+
continue
|
|
245
|
+
for turn_list in turns:
|
|
246
|
+
if not isinstance(turn_list, list):
|
|
247
|
+
continue
|
|
248
|
+
for turn in turn_list:
|
|
249
|
+
if not isinstance(turn, dict):
|
|
250
|
+
continue
|
|
251
|
+
if turn.get("role") == "user":
|
|
252
|
+
turns_out.append({
|
|
253
|
+
"content": turn.get("content", ""),
|
|
254
|
+
"time_anchor": turn.get("time_anchor"),
|
|
255
|
+
"question_type": turn.get("question_type"),
|
|
256
|
+
"plan": plan_name,
|
|
257
|
+
"batch": batch.get("batch_number"),
|
|
258
|
+
})
|
|
259
|
+
return turns_out
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def beam_rows_to_personas(rows: list[dict], *,
|
|
263
|
+
max_turns_per_persona: int = 100,
|
|
264
|
+
include_profile: bool = True) -> list[dict]:
|
|
265
|
+
"""Convert BEAM rows into the persona dict format the bench expects.
|
|
266
|
+
|
|
267
|
+
Returns list of: {user_id, text, facts} where:
|
|
268
|
+
user_id — derived from conversation_id
|
|
269
|
+
text — concatenation of user_profile (if include_profile=True)
|
|
270
|
+
+ the first N user turns. The user_profile contains the
|
|
271
|
+
ground-truth facts (Name, Age, Location, etc.) which the
|
|
272
|
+
μ=0 extractor should pull from the explicit "Name: X"
|
|
273
|
+
lines; the chat turns are the conversational context.
|
|
274
|
+
facts — ground-truth structured facts parsed from user_profile
|
|
275
|
+
(separate from text so the bench can check recall)
|
|
276
|
+
"""
|
|
277
|
+
personas = []
|
|
278
|
+
for row in rows:
|
|
279
|
+
conv_id = row.get("conversation_id", "unknown")
|
|
280
|
+
user_id = f"beam_{conv_id}"
|
|
281
|
+
facts = parse_user_facts(row)
|
|
282
|
+
turns = parse_chat_turns(row)
|
|
283
|
+
# cap turns to keep ingest tractable
|
|
284
|
+
turns = turns[:max_turns_per_persona]
|
|
285
|
+
# build the ingest text: profile first (so facts are stated
|
|
286
|
+
# explicitly), then the conversation turns
|
|
287
|
+
parts = []
|
|
288
|
+
if include_profile:
|
|
289
|
+
up = row.get("user_profile", {})
|
|
290
|
+
info = up.get("user_info", "")
|
|
291
|
+
rels = up.get("user_relationships", "")
|
|
292
|
+
if info:
|
|
293
|
+
parts.append(info)
|
|
294
|
+
if rels:
|
|
295
|
+
parts.append(rels)
|
|
296
|
+
# append the chat turns
|
|
297
|
+
for t in turns:
|
|
298
|
+
if t.get("content"):
|
|
299
|
+
parts.append(t["content"])
|
|
300
|
+
text = "\n".join(parts)
|
|
301
|
+
personas.append({
|
|
302
|
+
"user_id": user_id,
|
|
303
|
+
"text": text,
|
|
304
|
+
"facts": facts,
|
|
305
|
+
"n_turns": len(turns),
|
|
306
|
+
"conversation_id": conv_id,
|
|
307
|
+
})
|
|
308
|
+
return personas
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
__all__ = [
|
|
312
|
+
"load_beam_rows",
|
|
313
|
+
"parse_user_facts",
|
|
314
|
+
"parse_chat_turns",
|
|
315
|
+
"beam_rows_to_personas",
|
|
316
|
+
"DATASET_NAME",
|
|
317
|
+
]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""BEAM-style corpus generator — synthetic long-horizon conversations.
|
|
2
|
+
|
|
3
|
+
Mirrors the BEAM methodology (arXiv:2510.27246): auto-generated,
|
|
4
|
+
coherent, topically diverse multi-session conversations with
|
|
5
|
+
probing questions across 10 memory abilities. Deterministic (seeded);
|
|
6
|
+
persona timelines carry ground-truth registries used by the judge.
|
|
7
|
+
|
|
8
|
+
Buckets: 128K / 500K / 1M / 10M estimated tokens. Signal conversations
|
|
9
|
+
(persona sessions) are embedded in distractor noise (smalltalk +
|
|
10
|
+
long-form topical documents with competing capitalized entities) so the
|
|
11
|
+
answers are genuine needles — retrieval must beat brute-force context
|
|
12
|
+
stuffing, exactly the regime BEAM-10M targets.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import random
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
|
|
21
|
+
from cortexm.util import month_name, token_estimate
|
|
22
|
+
|
|
23
|
+
FIRST_NAMES = ["Alice", "Maya", "Priya", "Dana", "Elena", "Marcus", "Tom",
|
|
24
|
+
"Nadia", "Omar", "Julia", "Ken", "Sofia", "Ravi", "Chloe"]
|
|
25
|
+
LAST_NAMES = ["Johnson", "Chen", "Sharma", "Kovač", "Rossi", "Williams",
|
|
26
|
+
"Tanaka", "Silva", "Novak", "Brooks", "Garcia", "Osei"]
|
|
27
|
+
ORGS = ["Google", "Anthropic", "Stripe", "Microsoft", "Netflix", "Shopify",
|
|
28
|
+
"Figma", "Databricks", "Vercel", "Ramp", "Notion", "Linear"]
|
|
29
|
+
CITIES = ["Toronto", "Lisbon", "Austin", "Berlin", "Nairobi", "Seattle",
|
|
30
|
+
"Denver", "Prague", "Osaka", "Melbourne", "Zurich", "Austin"]
|
|
31
|
+
ROLES = ["software engineer", "product manager", "data scientist",
|
|
32
|
+
"designer", "engineering manager", "researcher", "DevOps engineer"]
|
|
33
|
+
TECH = ["Rust", "Python", "Go", "TypeScript", "Kotlin", "Swift"]
|
|
34
|
+
TEAMS = ["Platform", "Search", "Payments", "Growth", "Infra", "Mobile"]
|
|
35
|
+
PROJECTS = ["Project Falcon", "Project Atlas", "Project Beacon", "Project Cedar",
|
|
36
|
+
"Project Delta", "Project Echo", "Project Fable", "Project Granite"]
|
|
37
|
+
EVENTS = [("deployed the payment service", 3), ("ran the marathon", 8),
|
|
38
|
+
("adopted a cat", 2), ("spoke at a conference", 6),
|
|
39
|
+
("rebuilt the CI pipeline", 4), ("started piano lessons", 5),
|
|
40
|
+
("organized a hackathon", 9), ("published a blog post", 3)]
|
|
41
|
+
PREF_CATS = [
|
|
42
|
+
("coffee", ["espresso", "oat milk lattes", "cold brew", "green tea",
|
|
43
|
+
"black coffee", "matcha"]),
|
|
44
|
+
("music", ["jazz", "techno", "classical", "indie rock", "lo-fi beats"]),
|
|
45
|
+
("food", ["ramen", "pizza", "thai curry", "sushi", "tacos"]),
|
|
46
|
+
("editor theme", ["dark mode", "light mode"]),
|
|
47
|
+
]
|
|
48
|
+
HOBBIES = ["hiking", "rock climbing", "photography", "baking sourdough",
|
|
49
|
+
"kayaking", "chess", "birdwatching", "pottery"]
|
|
50
|
+
DISTRACTOR_TOPICS = [
|
|
51
|
+
"the weather", "a traffic jam", "a new smartphone release", "the stock market",
|
|
52
|
+
"a soccer match", "a Netflix series", "a recipe for lasagna", "airport delays",
|
|
53
|
+
"a coffee shop queue", "a podcast about history", "a piano concert",
|
|
54
|
+
"a neighbor's garden", "a marathon on TV", "a book about sailing",
|
|
55
|
+
]
|
|
56
|
+
WIKI_ENTITIES = [
|
|
57
|
+
"Mount Kilimanjaro", "the Baltic Sea", "Kafka Museum", "Antarctic Treaty",
|
|
58
|
+
"Hyperloop One", "Voyager Program", "Silk Road", "Lake Baikal",
|
|
59
|
+
"Gutenberg Press", "Mariana Trench", "Aurora Borealis", "Great Barrier Reef",
|
|
60
|
+
"Florence Cathedral", "Sahara Desert", "Yellowstone Park", "Aztec Calendar",
|
|
61
|
+
]
|
|
62
|
+
WIKI_VERBS = ["was documented", "attracted researchers", "made headlines",
|
|
63
|
+
"was rediscovered", "inspired a documentary", "broke records",
|
|
64
|
+
"was surveyed", "hosted a festival", "was renovated",
|
|
65
|
+
"surprised scientists"]
|
|
66
|
+
SMALLTALK = [
|
|
67
|
+
"Hey! How's it going today?",
|
|
68
|
+
"That sounds interesting, tell me more.",
|
|
69
|
+
"Oh wow, I didn't know that.",
|
|
70
|
+
"Thanks for the reminder!",
|
|
71
|
+
"Ha, that's funny.",
|
|
72
|
+
"Okay, got it.",
|
|
73
|
+
"Sounds like a plan.",
|
|
74
|
+
"Good morning! Ready for another day?",
|
|
75
|
+
"I was just thinking about something similar.",
|
|
76
|
+
"Cool, I'll keep that in mind.",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Persona:
|
|
82
|
+
user_id: str
|
|
83
|
+
full_name: str
|
|
84
|
+
first: str
|
|
85
|
+
nickname: str | None
|
|
86
|
+
employers: list = field(default_factory=list) # (org, start, end|None)
|
|
87
|
+
roles: list = field(default_factory=list)
|
|
88
|
+
cities: list = field(default_factory=list) # (city, start, end|None)
|
|
89
|
+
prefs: list = field(default_factory=list) # (cat, value, start, end|None)
|
|
90
|
+
skills: list = field(default_factory=list)
|
|
91
|
+
family: list = field(default_factory=list) # (name, relation)
|
|
92
|
+
manager: tuple | None = None # (name, team)
|
|
93
|
+
team_tech: tuple | None = None # (team, tech)
|
|
94
|
+
projects: list = field(default_factory=list) # (name, start, end|None)
|
|
95
|
+
events: list = field(default_factory=list) # (date, desc)
|
|
96
|
+
birthday: tuple | None = None
|
|
97
|
+
hobbies: list = field(default_factory=list)
|
|
98
|
+
instruction: tuple | None = None # (text, expected)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _d(y: int, m: int, day: int = 1) -> str:
|
|
102
|
+
return f"{y:04d}-{m:02d}-{day:02d}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def make_persona(rng: random.Random, idx: int, t0: datetime) -> Persona:
|
|
106
|
+
first = FIRST_NAMES[(idx * 5 + rng.randrange(3)) % len(FIRST_NAMES)]
|
|
107
|
+
last = LAST_NAMES[(idx * 7 + rng.randrange(3)) % len(LAST_NAMES)]
|
|
108
|
+
full = f"{first} {last}"
|
|
109
|
+
p = Persona(user_id=f"user{idx}", full_name=full, first=first,
|
|
110
|
+
nickname=first[:3].lower() if rng.random() < 0.5 else None)
|
|
111
|
+
|
|
112
|
+
# employment timeline: 2-3 orgs
|
|
113
|
+
n_jobs = rng.choice([2, 3])
|
|
114
|
+
orgs = rng.sample(ORGS, n_jobs + 1)
|
|
115
|
+
y = t0.year - 2
|
|
116
|
+
start = _d(y, rng.randrange(1, 10))
|
|
117
|
+
for i in range(n_jobs):
|
|
118
|
+
end = _d(y + 1 + i, rng.randrange(1, 12)) if i < n_jobs - 1 else None
|
|
119
|
+
p.employers.append((orgs[i], start, end))
|
|
120
|
+
start = end or start
|
|
121
|
+
p.roles.append((rng.choice(ROLES), p.employers[0][1], None))
|
|
122
|
+
|
|
123
|
+
# residence: 2 cities
|
|
124
|
+
cs = rng.sample(CITIES, 2)
|
|
125
|
+
p.cities = [(cs[0], _d(t0.year - 2, rng.randrange(1, 10)),
|
|
126
|
+
_d(t0.year - 1, rng.randrange(1, 12))),
|
|
127
|
+
(cs[1], _d(t0.year - 1, rng.randrange(1, 12)), None)]
|
|
128
|
+
|
|
129
|
+
# preferences with flips
|
|
130
|
+
for cat, vals in PREF_CATS[:rng.choice([2, 3])]:
|
|
131
|
+
v = rng.sample(vals, min(2, len(vals)))
|
|
132
|
+
p.prefs.append((cat, v[0], _d(t0.year - 2, rng.randrange(1, 12)),
|
|
133
|
+
_d(t0.year - 1, rng.randrange(1, 12))))
|
|
134
|
+
p.prefs.append((cat, v[1], _d(t0.year - 1, rng.randrange(1, 12)), None))
|
|
135
|
+
|
|
136
|
+
p.skills = rng.sample(TECH, 3)
|
|
137
|
+
sib = FIRST_NAMES[(idx * 3 + 7) % len(FIRST_NAMES)]
|
|
138
|
+
sib_last = LAST_NAMES[(idx * 11 + 5) % len(LAST_NAMES)]
|
|
139
|
+
p.family = [(f"{sib} {sib_last}", "sister")]
|
|
140
|
+
mgr = FIRST_NAMES[(idx * 9 + 2) % len(FIRST_NAMES)]
|
|
141
|
+
if mgr == first:
|
|
142
|
+
mgr = FIRST_NAMES[(idx * 9 + 3) % len(FIRST_NAMES)]
|
|
143
|
+
team = rng.choice(TEAMS)
|
|
144
|
+
p.manager = (mgr, team)
|
|
145
|
+
p.team_tech = (team, rng.choice(TECH))
|
|
146
|
+
p.projects = [(proj, _d(t0.year - 1 + i % 2, rng.randrange(1, 12)),
|
|
147
|
+
None if i % 2 else _d(t0.year, rng.randrange(1, 6)))
|
|
148
|
+
for i, proj in enumerate(rng.sample(PROJECTS, 3))]
|
|
149
|
+
evs = rng.sample(EVENTS, 4)
|
|
150
|
+
p.events = [(_d(t0.year - 1 + i % 2, rng.randrange(1, 12), rng.randrange(1, 28)),
|
|
151
|
+
ev[0]) for i, ev in enumerate(evs)]
|
|
152
|
+
p.birthday = (rng.randrange(1, 13), rng.randrange(1, 28))
|
|
153
|
+
p.hobbies = rng.sample(HOBBIES, 2)
|
|
154
|
+
p.instruction = ("Please always respond in French.", "French") \
|
|
155
|
+
if rng.random() < 0.6 else ("Always keep my answers short and concise.", "short")
|
|
156
|
+
return p
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _month(m: int) -> str:
|
|
160
|
+
return month_name(m)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def persona_messages(p: Persona, rng: random.Random, session_date: datetime,
|
|
164
|
+
part: int) -> list[tuple[str, str]]:
|
|
165
|
+
"""Surface the persona's life in natural, varied messages."""
|
|
166
|
+
msgs: list[tuple[str, str]] = []
|
|
167
|
+
A = lambda t: msgs.append(("user", t)) # noqa: E731
|
|
168
|
+
|
|
169
|
+
if part == 0:
|
|
170
|
+
# introduction session
|
|
171
|
+
A(f"My name is {p.full_name}.")
|
|
172
|
+
A(rng.choice([
|
|
173
|
+
f"I work at {p.employers[0][0]} as a {p.roles[0][0]}.",
|
|
174
|
+
f"I'm a {p.roles[0][0]} at {p.employers[0][0]}.",
|
|
175
|
+
f"I work as a {p.roles[0][0]} at {p.employers[0][0]}.",
|
|
176
|
+
]))
|
|
177
|
+
A(f"I live in {p.cities[0][0]}.")
|
|
178
|
+
b = p.birthday
|
|
179
|
+
A(f"My birthday is {_month(b[0])} {b[1]}.")
|
|
180
|
+
A(f"My sister {p.family[0][0]} lives nearby.")
|
|
181
|
+
if p.nickname:
|
|
182
|
+
A(f"But call me {p.nickname.capitalize() if len(p.nickname) > 2 else p.nickname}.")
|
|
183
|
+
A(p.instruction[0])
|
|
184
|
+
# state the first employer WITH its start date so the employment
|
|
185
|
+
# interval is recoverable from the text (TR "where did X work in
|
|
186
|
+
# YYYY" probes); otherwise valid_from defaults to the session date
|
|
187
|
+
# and the historical window is unanswerable from the corpus.
|
|
188
|
+
_e0 = p.employers[0]
|
|
189
|
+
A(f"I've been working at {_e0[0]} since "
|
|
190
|
+
f"{_month(int(_e0[1][5:7]))} {_e0[1][:4]}.")
|
|
191
|
+
if part == 1:
|
|
192
|
+
# preferences + skills
|
|
193
|
+
# state EVERY preference category (old -> new), so every PF probe
|
|
194
|
+
# is answerable from the conversation; part 7's "switched to X"
|
|
195
|
+
# then acts as a genuine re-statement / flip of the current value.
|
|
196
|
+
for i in range(0, len(p.prefs), 2):
|
|
197
|
+
cat, v_old = p.prefs[i][0], p.prefs[i][1]
|
|
198
|
+
v_new = p.prefs[i + 1][1] if i + 1 < len(p.prefs) else v_old
|
|
199
|
+
A(f"I prefer {v_new} over {v_old} for {cat}.")
|
|
200
|
+
for s in p.skills:
|
|
201
|
+
A(rng.choice([f"I know {s}.", f"I code in {s}.",
|
|
202
|
+
f"I've been learning {s}."]))
|
|
203
|
+
A(f"In my free time I {p.hobbies[0]}.")
|
|
204
|
+
if part == 2:
|
|
205
|
+
# job change
|
|
206
|
+
old = p.employers[0]
|
|
207
|
+
new = p.employers[1]
|
|
208
|
+
m_end = int(old[2][5:7]) if old[2] else 6
|
|
209
|
+
A(f"I left {old[0]} in {_month(m_end)}.")
|
|
210
|
+
m_new = int(new[1][5:7])
|
|
211
|
+
A(rng.choice([
|
|
212
|
+
f"I joined {new[0]} on {_month(m_new)} 5th, {new[1][:4]}.",
|
|
213
|
+
f"I started working at {new[0]} in {_month(m_new)} {new[1][:4]}.",
|
|
214
|
+
f"I'm now at {new[0]} as a {p.roles[0][0]}.",
|
|
215
|
+
]))
|
|
216
|
+
if len(p.employers) > 2:
|
|
217
|
+
mid, last = p.employers[1], p.employers[2]
|
|
218
|
+
m_mid = int(mid[2][5:7]) if mid[2] else 6
|
|
219
|
+
A(f"I left {mid[0]} in {_month(m_mid)} {mid[2][:4]}.")
|
|
220
|
+
A(f"These days I work at {last[0]}.")
|
|
221
|
+
if part == 3:
|
|
222
|
+
# move + family third-person
|
|
223
|
+
c_old, c_new = p.cities[0], p.cities[1]
|
|
224
|
+
m_move = int(c_new[1][5:7])
|
|
225
|
+
A(f"We moved to {c_new[0]} in {_month(m_move)} {c_new[1][:4]}.")
|
|
226
|
+
A(f"My sister {p.family[0][0]} works at {rng.choice(ORGS)}.")
|
|
227
|
+
if part == 4:
|
|
228
|
+
# work structure: manager + team + tech (multi-hop chain)
|
|
229
|
+
mgr, team = p.manager
|
|
230
|
+
tname, tech = p.team_tech
|
|
231
|
+
A(f"My manager is {mgr}.")
|
|
232
|
+
A(f"{mgr} manages the {tname} team.")
|
|
233
|
+
A(f"The {tname} team uses {tech} for everything.")
|
|
234
|
+
A(f"I'm on the {tname} team now.")
|
|
235
|
+
if part == 5:
|
|
236
|
+
# projects
|
|
237
|
+
for name, start, end in p.projects:
|
|
238
|
+
if end:
|
|
239
|
+
m = int(end[5:7])
|
|
240
|
+
A(rng.choice([
|
|
241
|
+
f"We shipped {name} in {_month(m)} {end[:4]}.",
|
|
242
|
+
f"I finished {name} last month.",
|
|
243
|
+
]))
|
|
244
|
+
else:
|
|
245
|
+
A(rng.choice([
|
|
246
|
+
f"I'm working on {name}.",
|
|
247
|
+
f"We're building {name} right now.",
|
|
248
|
+
f"I work on {name} with a few friends.",
|
|
249
|
+
]))
|
|
250
|
+
if part == 6:
|
|
251
|
+
# dated events
|
|
252
|
+
for date, desc in p.events[:2]:
|
|
253
|
+
m, day = int(date[5:7]), int(date[8:10])
|
|
254
|
+
A(rng.choice([
|
|
255
|
+
f"On {_month(m)} {day}, {date[:4]} I {desc}.",
|
|
256
|
+
f"I {desc} on {_month(m)} {day}, {date[:4]}.",
|
|
257
|
+
]))
|
|
258
|
+
A(rng.choice(SMALLTALK))
|
|
259
|
+
if part == 7:
|
|
260
|
+
for date, desc in p.events[2:]:
|
|
261
|
+
m, day = int(date[5:7]), int(date[8:10])
|
|
262
|
+
A(rng.choice([
|
|
263
|
+
f"On {_month(m)} {day}, {date[:4]}, I {desc}.",
|
|
264
|
+
f"I {desc} on {_month(m)} {day}, {date[:4]}.",
|
|
265
|
+
]))
|
|
266
|
+
# preference flip
|
|
267
|
+
cat = p.prefs[2][0] if len(p.prefs) > 2 else "coffee"
|
|
268
|
+
vals = [v for (c, v, s, e) in p.prefs if c == cat]
|
|
269
|
+
if len(vals) >= 2:
|
|
270
|
+
A(rng.choice([
|
|
271
|
+
f"Actually, I've switched to {vals[-1]}.",
|
|
272
|
+
f"These days I prefer {vals[-1]}.",
|
|
273
|
+
f"I'm more of a {vals[-1]} person now.",
|
|
274
|
+
]))
|
|
275
|
+
# conversational padding
|
|
276
|
+
for _ in range(rng.randrange(1, 3)):
|
|
277
|
+
A(rng.choice(SMALLTALK))
|
|
278
|
+
return msgs
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def distractor_paragraph(rng: random.Random) -> str:
|
|
282
|
+
"""Long-form topical distractor with competing capitalized entities."""
|
|
283
|
+
e1, e2 = rng.sample(WIKI_ENTITIES, 2)
|
|
284
|
+
verb1, verb2 = rng.sample(WIKI_VERBS, 2)
|
|
285
|
+
year = rng.randrange(1960, 2024)
|
|
286
|
+
topic = rng.choice(DISTRACTOR_TOPICS)
|
|
287
|
+
return (
|
|
288
|
+
f"I read an article about {e1} yesterday. Apparently {e1} {verb1} in {year}, "
|
|
289
|
+
f"and researchers compared it with {e2}, which {verb2} a decade earlier. "
|
|
290
|
+
f"The article also covered {topic}, and mentioned that {e2} remains a popular "
|
|
291
|
+
f"subject among historians. A guidebook author wrote that visiting {e1} takes "
|
|
292
|
+
f"about three days, while {e2} can be explored in an afternoon. "
|
|
293
|
+
f"Local officials say tourism around {e1} doubled since {year + 10}."
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def smalltalk_message(rng: random.Random) -> str:
|
|
298
|
+
return rng.choice(SMALLTALK)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass
|
|
302
|
+
class Corpus:
|
|
303
|
+
bucket: str
|
|
304
|
+
target_tokens: int
|
|
305
|
+
sessions: list = field(default_factory=list) # (user_id, date, messages)
|
|
306
|
+
personas: list = field(default_factory=list)
|
|
307
|
+
total_tokens: int = 0
|
|
308
|
+
generation_seconds: float = 0.0
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
BUCKETS = {
|
|
312
|
+
"128k": dict(target=128_000, personas=1, sessions=8, docs_factor=0.9),
|
|
313
|
+
"500k": dict(target=500_000, personas=2, sessions=12, docs_factor=1.0),
|
|
314
|
+
"1m": dict(target=1_000_000, personas=3, sessions=16, docs_factor=1.0),
|
|
315
|
+
"10m": dict(target=10_000_000, personas=6, sessions=40, docs_factor=1.0),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def generate(bucket: str, seed: int = 42, t0: datetime | None = None) -> Corpus:
|
|
320
|
+
import time
|
|
321
|
+
t_start = time.time()
|
|
322
|
+
cfg = BUCKETS[bucket]
|
|
323
|
+
rng = random.Random(seed)
|
|
324
|
+
t0 = t0 or datetime(2026, 3, 1, tzinfo=timezone.utc)
|
|
325
|
+
personas = [make_persona(rng, i, t0) for i in range(cfg["personas"])]
|
|
326
|
+
sessions = []
|
|
327
|
+
total = 0
|
|
328
|
+
|
|
329
|
+
n_sessions = cfg["sessions"]
|
|
330
|
+
for p in personas:
|
|
331
|
+
for s in range(n_sessions):
|
|
332
|
+
date = t0 + timedelta(days=s * 21 + rng.randrange(0, 5))
|
|
333
|
+
msgs = persona_messages(p, rng, date, s % 8)
|
|
334
|
+
if s == 0:
|
|
335
|
+
msgs = [("assistant", "Hi! I'm your assistant. Nice to meet you!")] + msgs
|
|
336
|
+
for r, txt in msgs:
|
|
337
|
+
total += token_estimate(txt)
|
|
338
|
+
sessions.append((p.user_id, date, msgs))
|
|
339
|
+
|
|
340
|
+
# distractor volume to reach the bucket target (measured, not estimated)
|
|
341
|
+
if cfg["target"] - total > 0:
|
|
342
|
+
step = max(1, len(sessions) // 24)
|
|
343
|
+
guard = 0
|
|
344
|
+
while total < cfg["target"] and guard < 500_000:
|
|
345
|
+
guard += 1
|
|
346
|
+
made = 0
|
|
347
|
+
for i in range(0, len(sessions), step):
|
|
348
|
+
if total >= cfg["target"]:
|
|
349
|
+
break
|
|
350
|
+
uid, date, msgs = sessions[i]
|
|
351
|
+
k = rng.randrange(0, max(1, len(msgs)))
|
|
352
|
+
if rng.random() < 0.45:
|
|
353
|
+
txt = distractor_paragraph(rng)
|
|
354
|
+
else:
|
|
355
|
+
txt = smalltalk_message(rng) + " " + smalltalk_message(rng)
|
|
356
|
+
msgs.insert(k, ("user", txt))
|
|
357
|
+
total += token_estimate(txt)
|
|
358
|
+
made += 1
|
|
359
|
+
if made == 0:
|
|
360
|
+
step = max(1, step - 1)
|
|
361
|
+
elif guard % 5 == 0 and step > 1:
|
|
362
|
+
step = max(1, step // 2)
|
|
363
|
+
# occasionally append pure-noise sessions
|
|
364
|
+
if rng.random() < 0.15:
|
|
365
|
+
batch = []
|
|
366
|
+
for _ in range(rng.randrange(6, 14)):
|
|
367
|
+
txt = (distractor_paragraph(rng) if rng.random() < 0.5
|
|
368
|
+
else smalltalk_message(rng) + " " + smalltalk_message(rng))
|
|
369
|
+
batch.append(("user", txt))
|
|
370
|
+
total += token_estimate(txt)
|
|
371
|
+
sessions.append((sessions[rng.randrange(len(sessions))][0],
|
|
372
|
+
t0 + timedelta(days=rng.randrange(400)), batch))
|
|
373
|
+
|
|
374
|
+
return Corpus(bucket=bucket, target_tokens=cfg["target"], sessions=sessions,
|
|
375
|
+
personas=personas, total_tokens=total,
|
|
376
|
+
generation_seconds=time.time() - t_start)
|