brainiac-cli 0.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. brain/__init__.py +39 -0
  2. brain/__main__.py +14 -0
  3. brain/_assets/AGENTS.md +564 -0
  4. brain/_assets/overlay/template/brand/brand-guide.md +24 -0
  5. brain/_assets/overlay/template/keywords/glossary.md +15 -0
  6. brain/_assets/overlay/template/people/roster.md +14 -0
  7. brain/_assets/overlay/template/voice/voice-profile.md +34 -0
  8. brain/_assets/routines/manifest.json +257 -0
  9. brain/_assets/scripts/brain-brief-mac.plist +59 -0
  10. brain/_assets/scripts/brain-brief.sh +74 -0
  11. brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
  12. brain/_assets/scripts/brain-synthesis.sh +214 -0
  13. brain/_assets/scripts/install-brief-mac.sh +152 -0
  14. brain/_assets/scripts/install-brief-windows.ps1 +97 -0
  15. brain/_assets/scripts/register_tasks.py +386 -0
  16. brain/_assets/templates/company.md +21 -0
  17. brain/_assets/templates/concept.md +27 -0
  18. brain/_assets/templates/daily.md +20 -0
  19. brain/_assets/templates/decision.md +42 -0
  20. brain/_assets/templates/meeting.md +33 -0
  21. brain/_assets/templates/person.md +20 -0
  22. brain/_assets/templates/project.md +23 -0
  23. brain/_assets/templates/state-moc.md +40 -0
  24. brain/_version.py +12 -0
  25. brain/anchor.py +121 -0
  26. brain/audit.py +422 -0
  27. brain/backup.py +210 -0
  28. brain/brief.py +417 -0
  29. brain/capture.py +117 -0
  30. brain/chunk.py +249 -0
  31. brain/classification.py +134 -0
  32. brain/cli.py +1906 -0
  33. brain/config.py +368 -0
  34. brain/connect.py +362 -0
  35. brain/context.py +108 -0
  36. brain/core.py +3018 -0
  37. brain/doctor.py +1161 -0
  38. brain/egress.py +148 -0
  39. brain/embed.py +857 -0
  40. brain/encryption.py +217 -0
  41. brain/frontmatter.py +102 -0
  42. brain/golden_probe.py +678 -0
  43. brain/graph.py +369 -0
  44. brain/graphify.py +352 -0
  45. brain/index.py +1576 -0
  46. brain/ingest/__init__.py +19 -0
  47. brain/ingest/handlers/__init__.py +43 -0
  48. brain/ingest/handlers/base.py +95 -0
  49. brain/ingest/handlers/docx.py +78 -0
  50. brain/ingest/handlers/email.py +228 -0
  51. brain/ingest/handlers/html.py +142 -0
  52. brain/ingest/handlers/image.py +91 -0
  53. brain/ingest/handlers/pdf.py +99 -0
  54. brain/ingest/handlers/pptx.py +69 -0
  55. brain/ingest/handlers/tables.py +41 -0
  56. brain/ingest/handlers/text.py +43 -0
  57. brain/ingest/handlers/xlsx.py +100 -0
  58. brain/ingest/handlers/zip.py +163 -0
  59. brain/ingest/pipeline.py +839 -0
  60. brain/ingest/transcript.py +158 -0
  61. brain/init.py +870 -0
  62. brain/maintenance.py +2266 -0
  63. brain/mcp_adapter.py +217 -0
  64. brain/multihop.py +232 -0
  65. brain/notes.py +195 -0
  66. brain/overlay.py +183 -0
  67. brain/projection.py +79 -0
  68. brain/rerank.py +425 -0
  69. brain/snapshot.py +231 -0
  70. brain/update.py +743 -0
  71. brain/vectors.py +225 -0
  72. brainiac_cli-0.16.0.dist-info/METADATA +306 -0
  73. brainiac_cli-0.16.0.dist-info/RECORD +77 -0
  74. brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
  75. brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
  76. brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
  77. brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
brain/embed.py ADDED
@@ -0,0 +1,857 @@
1
+ """Embedder ADAPTER INTERFACE + the real Arctic-embed embedder + an offline fallback.
2
+
3
+ Design of record (IDX-01) is Snowflake **Arctic-embed-m-v2.0** (305M, 768-d,
4
+ Apache-2.0) run **locally over ONNX Runtime via fastembed — NO PyTorch** (install
5
+ footprint + a Windows Defender win). Vectors are truncated to **MRL-256** for
6
+ storage (Matryoshka Representation Learning: the first 256 dims of the 768-d
7
+ vector carry most of the signal; we re-normalise after truncation).
8
+
9
+ Two implementations satisfy the ``Embedder`` protocol:
10
+
11
+ * ``ArcticEmbedder`` — the real model via ``fastembed.TextEmbedding``. Used
12
+ when fastembed + onnxruntime are importable AND the
13
+ model is locally available (bundled / already cached;
14
+ the Cowork egress allowlist excludes HuggingFace).
15
+ * ``HashEmbedder`` — a deterministic, network-free pseudo-embedder so the
16
+ index, retrieval, and tests run anywhere with no model
17
+ download. NOT semantically strong; a stand-in only.
18
+
19
+ Because both implement the protocol — including ``model_id`` and ``dim`` — the
20
+ index stores ``embed_model`` + ``embed_dim`` and forces a **clean rebuild** when
21
+ either changes (a HashEmbedder index must never be queried with Arctic vectors,
22
+ and vice-versa). Swapping the embedder is a one-line change at the call site.
23
+
24
+ Canonical task prefixes (IDX-02): Arctic-embed is asymmetric — queries are
25
+ embedded with the literal prefix ``query: `` and passages with no prefix. These
26
+ canonical prefixes are **never translated** (they are model tokens, not prose);
27
+ the in-language *contextual* prefix is a separate, content-level concern handled
28
+ in ``brain.chunk``.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import hashlib
33
+ import math
34
+ import os
35
+ import re
36
+ import sys
37
+ from typing import Protocol, Sequence, runtime_checkable
38
+
39
+ _TOKEN = re.compile(r"[A-Za-z0-9]+")
40
+
41
+ # Canonical Arctic-embed task prefix for queries (asymmetric retrieval). Passages
42
+ # carry no prefix. NEVER translate these — they are model control tokens.
43
+ QUERY_PREFIX = "query: "
44
+
45
+ # The model of record and its MRL storage dimension.
46
+ ARCTIC_MODEL_ID = "snowflake/snowflake-arctic-embed-m-v2.0"
47
+ ARCTIC_FULL_DIM = 768
48
+ MRL_DIM = 256
49
+
50
+
51
+ def _ort_threads() -> int | None:
52
+ """Intra-op thread count for the ONNX session (S11 speed fix).
53
+
54
+ Default: all physical cores (Apple Silicon has no SMT, so logical ==
55
+ physical — on the M4 Pro that is 12). Saturating the batch dimension
56
+ across cores is the biggest ORT knob for bulk embedding throughput.
57
+ Override via ``$BRAIN_EMBED_THREADS``."""
58
+ raw = os.environ.get("BRAIN_EMBED_THREADS")
59
+ if raw and raw.strip().isdigit():
60
+ return int(raw)
61
+ try:
62
+ return os.cpu_count() or None
63
+ except Exception:
64
+ return None
65
+
66
+
67
+ def _ort_providers() -> list[str]:
68
+ """ONNX Runtime execution providers for the embedder (S11 speed fix).
69
+
70
+ Default CPU-only (the safe, reproducible path — and the one the eval
71
+ gate ran on). On Apple Silicon, ``$BRAIN_EMBED_PROVIDERS=CoreMLExecutionProvider``
72
+ opts into the Apple Neural Engine / GPU, which can be much faster for
73
+ bulk encode — but CoreML compiles the model on first run and may fall
74
+ back per-op, so it is OPT-IN, not the default. Comma-separate for a
75
+ fallback chain (``CoreMLExecutionProvider,CPUExecutionProvider``)."""
76
+ raw = os.environ.get("BRAIN_EMBED_PROVIDERS")
77
+ if raw and raw.strip():
78
+ return [p.strip() for p in raw.split(",") if p.strip()]
79
+ return ["CPUExecutionProvider"]
80
+
81
+
82
+ def _embed_length_sorted(
83
+ prepared: list[str], embed_fn
84
+ ) -> list[list[float]]:
85
+ """Run a fastembed-style ``embed_fn(list_of_texts) -> generator of raw
86
+ vectors`` over ``prepared`` in LENGTH-SORTED batches, returning the raw
87
+ vectors in the ORIGINAL input order.
88
+
89
+ Why (S11 speed finding): fastembed pads every item in one ``embed()`` call to
90
+ the longest item in that call. On the real vault (mean chunk ~245 tokens,
91
+ long tail to ~845) a single bulk call pads everything to ~845 -> ~3-5x wasted
92
+ compute. Sorting by length and encoding in fixed-size batches means each
93
+ forward pass pads only to its local max -> the waste collapses. Measured
94
+ ~1.5x on the real vault (more on heavier-tailed corpora); the sort itself is
95
+ negligible vs encoding. Applies to EVERY embedder, including the incumbent
96
+ e5-small — a model-independent win. Batch size via ``$BRAIN_EMBED_BATCH``
97
+ (default 64; on CPU, bigger is not better — large batches thrash cache)."""
98
+ n = len(prepared)
99
+ if n <= 1:
100
+ return [list(v) for v in embed_fn(prepared)]
101
+ order = sorted(range(n), key=lambda i: len(prepared[i]))
102
+ batch = int(os.environ.get("BRAIN_EMBED_BATCH", "64"))
103
+ out: list[list[float] | None] = [None] * n
104
+ for i in range(0, n, batch):
105
+ idxs = order[i : i + batch]
106
+ vecs = list(embed_fn([prepared[j] for j in idxs]))
107
+ for j, v in zip(idxs, vecs):
108
+ out[j] = list(v)
109
+ return out # type: ignore[return-value]
110
+
111
+
112
+ @runtime_checkable
113
+ class Embedder(Protocol):
114
+ model_id: str
115
+ dim: int
116
+
117
+ def embed(self, text: str, *, is_query: bool = False) -> list[float]: ...
118
+ def embed_batch(
119
+ self, texts: Sequence[str], *, is_query: bool = False
120
+ ) -> list[list[float]]: ...
121
+
122
+
123
+ def _l2_normalise(vec: list[float]) -> list[float]:
124
+ norm = math.sqrt(sum(v * v for v in vec))
125
+ if norm == 0.0:
126
+ out = [0.0] * len(vec)
127
+ out[0] = 1.0
128
+ return out
129
+ return [v / norm for v in vec]
130
+
131
+
132
+ def mrl_truncate(vec: Sequence[float], dim: int = MRL_DIM) -> list[float]:
133
+ """Matryoshka truncation: take the first ``dim`` dims and re-normalise.
134
+
135
+ Arctic-embed-v2.0 is MRL-trained, so a 256-prefix of the 768-d vector is a
136
+ valid (smaller, faster-to-store) embedding once re-normalised to unit length.
137
+ """
138
+ return _l2_normalise(list(vec[:dim]))
139
+
140
+
141
+ class HashEmbedder:
142
+ """Deterministic bag-of-hashed-tokens embedding, L2-normalised.
143
+
144
+ NOT semantically strong — a stand-in that gives stable, reproducible vectors
145
+ for the index/retrieval contract and tests. Lexically-similar texts share
146
+ direction, which is enough to exercise the vector path end to end. ``is_query``
147
+ is accepted (protocol parity) but ignored — there is no asymmetry to model.
148
+ """
149
+
150
+ model_id = "hash-v1"
151
+
152
+ def __init__(self, dim: int = 384) -> None:
153
+ self.dim = dim
154
+
155
+ def embed(self, text: str, *, is_query: bool = False) -> list[float]:
156
+ vec = [0.0] * self.dim
157
+ for tok in _TOKEN.findall(text.lower()):
158
+ h = hashlib.sha256(tok.encode("utf-8")).digest()
159
+ idx = int.from_bytes(h[:4], "big") % self.dim
160
+ sign = 1.0 if h[4] & 1 else -1.0
161
+ vec[idx] += sign
162
+ return _l2_normalise(vec)
163
+
164
+ def embed_batch(
165
+ self, texts: Sequence[str], *, is_query: bool = False
166
+ ) -> list[list[float]]:
167
+ return [self.embed(t, is_query=is_query) for t in texts]
168
+
169
+
170
+ class ArcticEmbedder:
171
+ """Snowflake Arctic-embed-m-v2.0 via fastembed/ONNX — NO PyTorch.
172
+
173
+ Lazy: the ONNX model is loaded on first ``embed``/``embed_batch`` so merely
174
+ constructing the embedder (to read ``model_id``/``dim`` for a meta check) is
175
+ cheap and offline. Raises ``EmbedderUnavailable`` if fastembed/onnxruntime
176
+ is not importable.
177
+ """
178
+
179
+ def __init__(
180
+ self,
181
+ model_id: str = ARCTIC_MODEL_ID,
182
+ dim: int = MRL_DIM,
183
+ *,
184
+ cache_dir: str | None = None,
185
+ ) -> None:
186
+ self.model_id = model_id
187
+ self.dim = dim # MRL storage dim
188
+ # Bundled-model path (S06 / INT-02): on the Cowork VM, HuggingFace is NOT
189
+ # on the egress allowlist, so the model is shipped in the workspace and
190
+ # ``$BRAIN_MODEL_CACHE`` points fastembed at that mounted cache dir. ONNX
191
+ # Runtime memory-maps the model file from there — read in place from the
192
+ # mount, never copied to /tmp.
193
+ self._cache_dir = cache_dir or os.environ.get("BRAIN_MODEL_CACHE")
194
+ self._model = None # lazily created TextEmbedding
195
+
196
+ @staticmethod
197
+ def available() -> bool:
198
+ try:
199
+ import fastembed # noqa: F401
200
+ import onnxruntime # noqa: F401
201
+
202
+ return True
203
+ except Exception:
204
+ return False
205
+
206
+ def _ensure_model(self):
207
+ if self._model is None:
208
+ try:
209
+ from fastembed import TextEmbedding
210
+ except Exception as exc: # pragma: no cover - exercised when absent
211
+ raise EmbedderUnavailable(
212
+ "fastembed/onnxruntime not importable; install the 'embed' "
213
+ "extra or bundle the model"
214
+ ) from exc
215
+ self._model = TextEmbedding(
216
+ model_name=self.model_id, cache_dir=self._cache_dir,
217
+ threads=_ort_threads(), providers=_ort_providers(),
218
+ )
219
+ return self._model
220
+
221
+ def _encode(self, texts: list[str], is_query: bool) -> list[list[float]]:
222
+ model = self._ensure_model()
223
+ prepared = [(QUERY_PREFIX + t) if is_query else t for t in texts]
224
+ return [mrl_truncate(v, self.dim) for v in _embed_length_sorted(prepared, model.embed)]
225
+
226
+ def embed(self, text: str, *, is_query: bool = False) -> list[float]:
227
+ return self._encode([text], is_query)[0]
228
+
229
+ def embed_batch(
230
+ self, texts: Sequence[str], *, is_query: bool = False
231
+ ) -> list[list[float]]:
232
+ return self._encode(list(texts), is_query)
233
+
234
+
235
+ class CatalogEmbedder:
236
+ """Any fastembed-CATALOGUED model via ONNX — a real-semantic embedder.
237
+
238
+ Design-of-record is Arctic-embed-m-v2.0, but that exact checkpoint is NOT in
239
+ the fastembed catalog (flagged S03). This adapter runs any *catalogued*
240
+ model (``intfloat/multilingual-e5-*``,
241
+ ``sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2``, the
242
+ in-catalog Arctic variants, …) so a real multilingual embedder can be used
243
+ as a transparent proxy until the production checkpoint is bundled.
244
+
245
+ Activated ONLY when ``$BRAIN_EMBED_MODEL`` is set (see ``get_embedder``) — so
246
+ default behaviour is unchanged. ``$BRAIN_EMBED_DIM`` (default 384) declares
247
+ the storage dim WITHOUT loading the model (the index reads ``.dim`` before
248
+ any embed call, for the model-change guard). ``$BRAIN_MODEL_CACHE`` /
249
+ ``$BRAIN_FASTEMBED_CACHE`` point fastembed at a local cache (offline-capable).
250
+
251
+ e5-family models are asymmetric (``query:`` / ``passage:`` prefixes); the
252
+ paraphrase-MiniLM family is symmetric. The right prefix scheme is selected
253
+ from the model id so cross-lingual retrieval is not silently degraded.
254
+ """
255
+
256
+ def __init__(
257
+ self,
258
+ model_id: str,
259
+ dim: int | None = None,
260
+ *,
261
+ cache_dir: str | None = None,
262
+ ) -> None:
263
+ self.model_id = model_id
264
+ self.dim = int(dim if dim is not None else os.environ.get("BRAIN_EMBED_DIM", 384))
265
+ self._cache_dir = (
266
+ cache_dir
267
+ or os.environ.get("BRAIN_MODEL_CACHE")
268
+ or os.environ.get("BRAIN_FASTEMBED_CACHE")
269
+ )
270
+ self._model = None
271
+ low = model_id.lower()
272
+ self._is_e5 = "e5" in low # e5 family uses query:/passage: prefixes
273
+
274
+ @staticmethod
275
+ def available() -> bool:
276
+ return ArcticEmbedder.available()
277
+
278
+ # ONNX-only models NOT in the stock fastembed catalog but registerable via
279
+ # add_custom_model (HF repo carries an ONNX export). Lets brain use, e.g.,
280
+ # multilingual-e5-small — the EXACT model Smart Connections uses — for a true
281
+ # apples-to-apples eval. mean-pooled + L2-normalised, query:/passage: prefixed.
282
+ _CUSTOM_MODELS = {
283
+ "intfloat/multilingual-e5-small": {
284
+ "hf": "Xenova/multilingual-e5-small", "dim": 384,
285
+ "model_file": "onnx/model.onnx",
286
+ },
287
+ "intfloat/multilingual-e5-base": {
288
+ "hf": "Xenova/multilingual-e5-base", "dim": 768,
289
+ "model_file": "onnx/model.onnx",
290
+ },
291
+ }
292
+
293
+ def _register_custom(self) -> bool:
294
+ spec = self._CUSTOM_MODELS.get(self.model_id)
295
+ if not spec:
296
+ return False
297
+ from fastembed import TextEmbedding
298
+ from fastembed.common.model_description import ModelSource, PoolingType
299
+ try:
300
+ TextEmbedding.add_custom_model(
301
+ model=self.model_id, pooling=PoolingType.MEAN, normalization=True,
302
+ sources=ModelSource(hf=spec["hf"]), dim=spec["dim"],
303
+ model_file=spec.get("model_file", "onnx/model.onnx"),
304
+ )
305
+ except Exception:
306
+ # Already registered (idempotent) or registry quirk — fall through and
307
+ # let TextEmbedding() surface a real error if the model truly isn't usable.
308
+ pass
309
+ return True
310
+
311
+ def _ensure_model(self):
312
+ if self._model is None:
313
+ try:
314
+ from fastembed import TextEmbedding
315
+ except Exception as exc: # pragma: no cover
316
+ raise EmbedderUnavailable(
317
+ "fastembed/onnxruntime not importable for CatalogEmbedder"
318
+ ) from exc
319
+ self._register_custom()
320
+ self._model = TextEmbedding(
321
+ model_name=self.model_id, cache_dir=self._cache_dir,
322
+ threads=_ort_threads(), providers=_ort_providers(),
323
+ )
324
+ return self._model
325
+
326
+ def _prep(self, text: str, is_query: bool) -> str:
327
+ if self._is_e5:
328
+ return ("query: " if is_query else "passage: ") + text
329
+ return text
330
+
331
+ def _encode(self, texts: list[str], is_query: bool) -> list[list[float]]:
332
+ model = self._ensure_model()
333
+ prepared = [self._prep(t, is_query) for t in texts]
334
+ out: list[list[float]] = []
335
+ for v in _embed_length_sorted(prepared, model.embed):
336
+ # If the model emits a wider vector than declared, MRL-truncate +
337
+ # renormalise to the declared storage dim; if narrower, keep as-is.
338
+ out.append(_l2_normalise(v[: self.dim]) if len(v) >= self.dim else _l2_normalise(v))
339
+ return out
340
+
341
+ def embed(self, text: str, *, is_query: bool = False) -> list[float]:
342
+ return self._encode([text], is_query)[0]
343
+
344
+ def embed_batch(
345
+ self, texts: Sequence[str], *, is_query: bool = False
346
+ ) -> list[list[float]]:
347
+ return self._encode(list(texts), is_query)
348
+
349
+
350
+ # --- Direct-ONNX embedder of record (DIST-01: eliminates fastembed) ---
351
+ # intfloat/multilingual-e5-small (Apache-2.0) is the S10/S11-closed model of
352
+ # record. The ONNX export at Xenova/multilingual-e5-small is a standard BERT-
353
+ # style encoder: inputs (input_ids, attention_mask, token_type_ids), output
354
+ # last_hidden_state [batch, seq, 384]. Embedding = MEAN-POOL over the attention
355
+ # mask + L2-normalise. Loaded DIRECTLY via onnxruntime + tokenizers — NO
356
+ # fastembed, NO PyTorch. This is the minimal-dep path the corporate build uses;
357
+ # it mirrors OnnxReranker's approach.
358
+ E5_SMALL_ONNX_REPO = "Xenova/multilingual-e5-small"
359
+ E5_SMALL_ONNX_FILE = "onnx/model.onnx"
360
+ E5_SMALL_MODEL_ID = "intfloat/multilingual-e5-small"
361
+ E5_SMALL_DIM = 384
362
+
363
+ # int8-quantized variant (S09/PF-01 — latency optimization). Produced OFFLINE
364
+ # by ``eval/int8_quantize_e5.py`` (onnxruntime.quantization.quantize_dynamic,
365
+ # weight-only dynamic int8, QInt8, per-channel MatMul) from the SAME
366
+ # Xenova/multilingual-e5-small fp32 export — never downloaded pre-quantized,
367
+ # so provenance is fully reproducible in-repo. Same tokenizer, same 384-d
368
+ # mean-pooled + L2-normalised output contract; ONLY the ONNX weights/ops
369
+ # differ. Kept behind an explicit opt-in (constructor arg or
370
+ # ``$BRAIN_EMBED_QUANT=int8``) — the IMPLICIT default stays fp32 (KILL-SWITCH:
371
+ # omit the flag, or set it to ``fp32``, to get the unchanged production path).
372
+ E5_SMALL_INT8_ONNX_FILE = "onnx/model_int8.onnx"
373
+ _VALID_QUANTIZATIONS = ("fp32", "int8")
374
+
375
+
376
+ class OnnxEmbedder:
377
+ """multilingual-e5-small loaded DIRECTLY via ONNX Runtime — no fastembed.
378
+
379
+ This is the model-of-record embedder for the corporate, minimal-dependency
380
+ build (DIST-01). e5-small is an asymmetric encoder: queries carry the
381
+ ``query: `` prefix, passages carry ``passage: ``. Output is mean-pooled over
382
+ the attention mask and L2-normalised to a 384-d vector.
383
+
384
+ Lazy: the ONNX session + tokenizer are created on first ``embed`` so
385
+ constructing the embedder (to read ``model_id``/``dim`` for the index
386
+ model-change guard) is cheap and offline. Raises ``EmbedderUnavailable``
387
+ if onnxruntime/tokenizers is not importable or the model is unavailable.
388
+
389
+ Offline-first: set ``$BRAIN_MODEL_CACHE`` (or pass ``local_dir``) to point
390
+ at a bundled/snapshot model dir so NO HuggingFace download is attempted.
391
+
392
+ ``quantization`` (S09/PF-01): ``"fp32"`` (default — the shipped production
393
+ weights, UNCHANGED behaviour) or ``"int8"`` (opt-in — loads
394
+ ``onnx/model_int8.onnx`` from the same ``local_dir`` instead, and reports a
395
+ distinct ``model_id`` suffixed ``-int8`` so the index's model-change guard
396
+ forces a clean rebuild before an int8-embedded index is ever queried
397
+ against fp32 vectors, or vice versa — same contract as any other embedder
398
+ swap). Resolution order: explicit ``quantization=`` arg >
399
+ ``$BRAIN_EMBED_QUANT`` > ``"fp32"``. This is the non-destructive
400
+ kill-switch: production callers that never set the env var or pass the
401
+ arg get exactly the pre-S09 fp32 behaviour.
402
+ """
403
+
404
+ def __init__(
405
+ self,
406
+ *,
407
+ hf_repo: str | None = None,
408
+ onnx_file: str | None = None,
409
+ local_dir: str | None = None,
410
+ model_id: str | None = None,
411
+ dim: int | None = None,
412
+ cache_dir: str | None = None,
413
+ quantization: str | None = None,
414
+ ) -> None:
415
+ self.quantization = (
416
+ quantization or os.environ.get("BRAIN_EMBED_QUANT") or "fp32"
417
+ ).strip().lower()
418
+ if self.quantization not in _VALID_QUANTIZATIONS:
419
+ raise ValueError(
420
+ f"OnnxEmbedder: quantization={self.quantization!r} not in "
421
+ f"{_VALID_QUANTIZATIONS!r}"
422
+ )
423
+ is_int8 = self.quantization == "int8"
424
+ default_onnx_file = E5_SMALL_INT8_ONNX_FILE if is_int8 else E5_SMALL_ONNX_FILE
425
+ default_model_id = (E5_SMALL_MODEL_ID + "-int8") if is_int8 else E5_SMALL_MODEL_ID
426
+ self.model_id = model_id or default_model_id
427
+ self.dim = int(dim if dim is not None else os.environ.get("BRAIN_EMBED_DIM", E5_SMALL_DIM))
428
+ self._hf_repo = hf_repo or E5_SMALL_ONNX_REPO
429
+ self._onnx_file = onnx_file or default_onnx_file
430
+ self._local_dir = (
431
+ local_dir
432
+ or cache_dir
433
+ or os.environ.get("BRAIN_MODEL_CACHE")
434
+ or os.environ.get("BRAIN_EMBED_ONNX_DIR")
435
+ )
436
+ self._sess = None
437
+ self._tok = None
438
+ self._in_names: list[str] | None = None
439
+
440
+ @staticmethod
441
+ def available() -> bool:
442
+ try:
443
+ import onnxruntime # noqa: F401
444
+ import tokenizers # noqa: F401
445
+
446
+ return True
447
+ except Exception:
448
+ return False
449
+
450
+ def _ensure(self):
451
+ if self._sess is None:
452
+ try:
453
+ import onnxruntime as ort
454
+ from tokenizers import Tokenizer
455
+ except Exception as exc: # pragma: no cover
456
+ raise EmbedderUnavailable(
457
+ "onnxruntime/tokenizers not importable for OnnxEmbedder"
458
+ ) from exc
459
+ try:
460
+ onnx_path, base = self._resolve_model_files()
461
+ so = ort.SessionOptions()
462
+ so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
463
+ t = _ort_threads()
464
+ if t:
465
+ so.intra_op_num_threads = t
466
+ self._sess = ort.InferenceSession(
467
+ onnx_path, sess_options=so, providers=_ort_providers()
468
+ )
469
+ self._in_names = [i.name for i in self._sess.get_inputs()]
470
+ self._tok = Tokenizer.from_file(os.path.join(base, "tokenizer.json"))
471
+ # Truncate to the model's context window BEFORE padding. e5-small
472
+ # is a BERT encoder with max_position_embeddings=512; feeding a
473
+ # longer sequence makes the position-embedding Add node fail to
474
+ # broadcast ("512 by 620") and crashes the whole rebuild. The
475
+ # char-based chunk ceiling (chunk.MAX_CHARS) does NOT guarantee
476
+ # <=512 tokens (dense PT/ES or code text tokenises >2x), so the
477
+ # embedder MUST clamp — the same default fastembed/sentence-
478
+ # transformers apply. Override via $BRAIN_EMBED_MAX_TOKENS.
479
+ _max_tok = 512
480
+ try:
481
+ _mt = os.environ.get("BRAIN_EMBED_MAX_TOKENS")
482
+ if _mt and _mt.strip().isdigit():
483
+ _max_tok = int(_mt)
484
+ except Exception:
485
+ pass
486
+ self._tok.enable_truncation(max_length=_max_tok)
487
+ # Pad to the longest in a batch so encode_batch returns a
488
+ # rectangular [batch, seq] int matrix (not ragged lists).
489
+ self._tok.enable_padding(pad_id=0, pad_token="<pad>")
490
+ except EmbedderUnavailable:
491
+ raise
492
+ except Exception as exc: # pragma: no cover - model unavailable offline
493
+ raise EmbedderUnavailable(
494
+ f"ONNX embedder {self.model_id!r} unavailable: {exc}"
495
+ ) from exc
496
+ return self._sess
497
+
498
+ def _resolve_model_files(self) -> tuple[str, str]:
499
+ """Resolve (onnx_path, base_dir) across three offline/online layouts:
500
+
501
+ 1. ``local_dir`` is a SNAPSHOT dir containing ``onnx/model.onnx`` +
502
+ ``tokenizer.json`` directly (the bundled / vendored layout, and the
503
+ resolved-snapshot layout). Preferred — no HF dep at runtime.
504
+ 2. ``local_dir`` is an HF-style cache ROOT (contains
505
+ ``models--Xenova--multilingual-e5-small/``): resolve the latest
506
+ snapshot via ``huggingface_hub.snapshot_download(cache_dir=...)``.
507
+ 3. No ``local_dir``: download from HF (online) via ``snapshot_download``.
508
+ """
509
+ pat = [
510
+ self._onnx_file,
511
+ self._onnx_file + "_data",
512
+ "tokenizer*",
513
+ "*.json",
514
+ ]
515
+ if self._local_dir:
516
+ direct = os.path.join(self._local_dir, self._onnx_file)
517
+ if os.path.exists(direct) and os.path.exists(
518
+ os.path.join(self._local_dir, "tokenizer.json")
519
+ ):
520
+ return direct, self._local_dir
521
+ # Direct .onnx file path as local_dir.
522
+ if os.path.isfile(self._local_dir) and self._local_dir.endswith(".onnx"):
523
+ return self._local_dir, os.path.dirname(self._local_dir)
524
+ # HF cache root: contains models--<org>--<name>/.
525
+ repo_dir = "models--" + self._hf_repo.replace("/", "--")
526
+ if os.path.isdir(os.path.join(self._local_dir, repo_dir)):
527
+ from huggingface_hub import snapshot_download
528
+
529
+ base = snapshot_download(
530
+ self._hf_repo, cache_dir=self._local_dir, allow_patterns=pat,
531
+ )
532
+ return os.path.join(base, self._onnx_file), base
533
+ raise EmbedderUnavailable(
534
+ f"local_dir {self._local_dir!r} is neither a snapshot dir nor an "
535
+ f"HF cache root for {self._hf_repo!r}"
536
+ )
537
+ from huggingface_hub import snapshot_download
538
+
539
+ base = snapshot_download(self._hf_repo, allow_patterns=pat)
540
+ return os.path.join(base, self._onnx_file), base
541
+
542
+ def _encode_raw(self, texts: list[str]) -> list[list[float]]:
543
+ """Mean-pool last_hidden_state over the attention mask + L2-normalise."""
544
+ import numpy as np
545
+
546
+ self._ensure()
547
+ enc = self._tok.encode_batch(texts)
548
+ ii = np.array([e.ids for e in enc], dtype=np.int64)
549
+ am = np.array([e.attention_mask for e in enc], dtype=np.int64)
550
+ feed: dict[str, object] = {}
551
+ for nm in self._in_names:
552
+ low = nm.lower()
553
+ if "input_id" in low:
554
+ feed[nm] = ii
555
+ elif "attention" in low:
556
+ feed[nm] = am
557
+ elif "token_type" in low:
558
+ feed[nm] = np.zeros_like(ii)
559
+ hidden = self._sess.run(None, feed)[0] # [batch, seq, dim] float32
560
+ # Mean pool: sum(hidden * mask) / sum(mask), per item.
561
+ mask = am.astype(hidden.dtype)[:, :, None] # [batch, seq, 1]
562
+ summed = (hidden * mask).sum(axis=1) # [batch, dim]
563
+ counts = mask.sum(axis=1)
564
+ counts = np.maximum(counts, 1.0) # avoid div-by-zero
565
+ pooled = summed / counts
566
+ out: list[list[float]] = []
567
+ for v in pooled:
568
+ d = v[: self.dim] if v.shape[0] >= self.dim else v
569
+ out.append(_l2_normalise(list(d)))
570
+ return out
571
+
572
+ def _encode(self, texts: list[str], is_query: bool) -> list[list[float]]:
573
+ # e5-family asymmetry: query: / passage: prefixes (model control tokens;
574
+ # never translated). mean-pool + L2-norm happens in _encode_raw.
575
+ prepared = [("query: " if is_query else "passage: ") + t for t in texts]
576
+ return [v for v in _embed_length_sorted(prepared, self._encode_raw)]
577
+
578
+ def embed(self, text: str, *, is_query: bool = False) -> list[float]:
579
+ return self._encode([text], is_query)[0]
580
+
581
+ def embed_batch(
582
+ self, texts: Sequence[str], *, is_query: bool = False
583
+ ) -> list[list[float]]:
584
+ return self._encode(list(texts), is_query)
585
+
586
+
587
+ class QwenEmbedder:
588
+ """Qwen3-Embedding-0.6B via the ``qwen3-embed`` lib (ONNX INT8, Apache-2.0).
589
+
590
+ The 2025-2026 best-in-class small multilingual embedder (MMTEB 64.33, 32K
591
+ context, 100+ langs incl. EN/PT/ES, 1024-d, MRL-truncatable). Shipped in S11
592
+ (UPG-02) as the upgrade over ``multilingual-e5-small``.
593
+
594
+ ``qwen3-embed`` (n24q02m fork) is a fastembed-compatible lib that loads the
595
+ ONNX INT8 export (~573 MB) — chosen because fastembed ≤ 0.8.0 (the only
596
+ released series as of 2026-06) does NOT catalogue Qwen3, and PR #605 is not
597
+ yet released. Lazy: the ONNX session is created on first ``embed`` so
598
+ constructing the embedder (to read ``model_id``/``dim`` for the meta check)
599
+ is cheap and offline. Raises ``EmbedderUnavailable`` if the lib/onnxruntime
600
+ is not importable.
601
+
602
+ Qwen3-Embedding is an instruction-tuned decoder embedder: queries MUST carry
603
+ an instruction prefix (``Instruct: ...\\nQuery: ``) for optimal ranking
604
+ (empirically verified 2026-06-28: the prefix widens the relevant-vs-irrelevant
605
+ cosine margin 0.30 -> 0.40). Passages carry NO prefix. This is the same class
606
+ of asymmetry as e5 ``query:``/``passage:``, and the same class of bug if
607
+ omitted (silent ranking degradation). The prefix is model-specific, so it is
608
+ applied here, INSIDE the adapter — the caller passes raw text.
609
+ """
610
+
611
+ # The Qwen3-Embedding default retrieval instruction (from the tech report
612
+ # ablations — the generic web-search instruction is the recommended default).
613
+ _QUERY_INSTRUCT = (
614
+ "Instruct: Given a web search query, retrieve relevant passages that "
615
+ "answer the query.\nQuery: "
616
+ )
617
+
618
+ def __init__(
619
+ self,
620
+ model_id: str = "n24q02m/Qwen3-Embedding-0.6B-ONNX",
621
+ dim: int | None = None,
622
+ *,
623
+ cache_dir: str | None = None,
624
+ ) -> None:
625
+ self.model_id = model_id
626
+ # default 1024 (full); MRL-truncate to a smaller dim via BRAIN_EMBED_DIM
627
+ self.dim = int(dim if dim is not None else os.environ.get("BRAIN_EMBED_DIM", 1024))
628
+ self._cache_dir = (
629
+ cache_dir
630
+ or os.environ.get("BRAIN_MODEL_CACHE")
631
+ or os.environ.get("BRAIN_FASTEMBED_CACHE")
632
+ )
633
+ self._model = None
634
+
635
+ @staticmethod
636
+ def available() -> bool:
637
+ try:
638
+ import qwen3_embed # noqa: F401
639
+ import onnxruntime # noqa: F401
640
+
641
+ return True
642
+ except Exception:
643
+ return False
644
+
645
+ def _ensure_model(self):
646
+ if self._model is None:
647
+ try:
648
+ from qwen3_embed import TextEmbedding
649
+ except Exception as exc: # pragma: no cover - exercised when absent
650
+ raise EmbedderUnavailable(
651
+ "qwen3-embed/onnxruntime not importable; pip install qwen3-embed"
652
+ ) from exc
653
+ self._model = TextEmbedding(
654
+ model_name=self.model_id, cache_dir=self._cache_dir,
655
+ threads=_ort_threads(), providers=_ort_providers(),
656
+ )
657
+ return self._model
658
+
659
+ def _encode(self, texts: list[str], is_query: bool) -> list[list[float]]:
660
+ model = self._ensure_model()
661
+ prepared = [(self._QUERY_INSTRUCT + t) if is_query else t for t in texts]
662
+ out: list[list[float]] = []
663
+ for v in _embed_length_sorted(prepared, model.embed):
664
+ # MRL-truncate + renormalise to the declared storage dim if the model
665
+ # emits a wider vector (1024 full -> 256/512 storage). Qwen3-Embedding
666
+ # is MRL-trained, so a prefix is a valid smaller embedding.
667
+ out.append(_l2_normalise(v[: self.dim]) if len(v) >= self.dim else _l2_normalise(v))
668
+ return out
669
+
670
+ def embed(self, text: str, *, is_query: bool = False) -> list[float]:
671
+ return self._encode([text], is_query)[0]
672
+
673
+ def embed_batch(
674
+ self, texts: Sequence[str], *, is_query: bool = False
675
+ ) -> list[list[float]]:
676
+ return self._encode(list(texts), is_query)
677
+
678
+
679
+ def is_qwen_model(model_id: str) -> bool:
680
+ """True if the model id names a Qwen3-Embedding model (the qwen3-embed lib path)."""
681
+ low = (model_id or "").lower()
682
+ return "qwen3-embedding" in low or "qwen3-embed" in low or "qwen-embed" in low
683
+
684
+
685
+ class EmbedderUnavailable(RuntimeError):
686
+ """Raised when the requested embedder backend is not importable/usable."""
687
+
688
+
689
+ # The implicit ``auto``-path degrade to the non-semantic ``HashEmbedder`` is the
690
+ # single most dangerous silent failure in the stack: retrieval keeps "working"
691
+ # but answers with garbage vectors, and ``brain status`` still reports the
692
+ # INDEX's recorded embed_model (metadata), not the live embedder actually in use.
693
+ # Discovered S11 (dual-run parity) when a venv without ``onnxruntime`` silently
694
+ # ran the whole integrity scan on hash vectors and found zero near-dups. So the
695
+ # implicit fallback is now LOUD, and fail-closable for production/clean-machine.
696
+ _HASH_FALLBACK_MSG = (
697
+ "brain: WARNING — no real semantic embedder is available "
698
+ "(onnxruntime/tokenizers not importable or the e5-small ONNX model is "
699
+ "missing), so retrieval is FALLING BACK to the non-semantic HashEmbedder. "
700
+ "Search/near-dup quality will be effectively random. Install the 'corporate' "
701
+ "extras (onnxruntime + tokenizers) or the bundled model. Set "
702
+ "BRAIN_REQUIRE_REAL_EMBEDDER=1 to fail closed instead of degrading; set "
703
+ "BRAIN_EMBEDDER=hash to select the hash embedder explicitly and silence this."
704
+ )
705
+
706
+
707
+ def _implicit_hash_fallback() -> Embedder:
708
+ """Return HashEmbedder for the IMPLICIT auto-path, but never silently:
709
+ fail closed when the operator demanded a real embedder, else warn loudly."""
710
+ if os.environ.get("BRAIN_REQUIRE_REAL_EMBEDDER"):
711
+ raise EmbedderUnavailable(
712
+ "BRAIN_REQUIRE_REAL_EMBEDDER is set but no real semantic embedder is "
713
+ "available (onnxruntime/tokenizers missing or e5-small model absent). "
714
+ "Refusing to degrade to the non-semantic HashEmbedder."
715
+ )
716
+ print(_HASH_FALLBACK_MSG, file=sys.stderr, flush=True)
717
+ return HashEmbedder()
718
+
719
+
720
+ def get_embedder(prefer: str = "auto") -> Embedder:
721
+ """Adapter selection.
722
+
723
+ ``hash`` forces the offline fallback (tests, CI) — EXPLICIT, no warning.
724
+ ``onnx`` selects ``OnnxEmbedder`` — the direct-ONNX model-of-record
725
+ (e5-small, Apache-2.0); this is the MINIMAL-DEPENDENCY path the corporate
726
+ build uses (DIST-01: no fastembed, no PyTorch). ``onnx-int8`` (S09/PF-01)
727
+ selects the SAME ``OnnxEmbedder`` with ``quantization="int8"`` — an
728
+ explicit opt-in only; ``onnx``/``auto`` are UNCHANGED and stay fp32 unless
729
+ ``$BRAIN_EMBED_QUANT=int8`` is also set (the non-destructive kill-switch:
730
+ default behaviour never changes). ``arctic``/``catalog``/``qwen`` select
731
+ the legacy fastembed/qwen3-embed paths (kept for A/B only; NOT in the
732
+ corporate build). ``auto`` honours ``$BRAIN_EMBED_MODEL`` when it names a
733
+ Qwen model, else prefers the direct-ONNX ``OnnxEmbedder`` (e5-small,
734
+ fp32 unless ``$BRAIN_EMBED_QUANT=int8``), else degrades to HashEmbedder —
735
+ but that IMPLICIT degrade is never silent (S11): it warns to stderr, or
736
+ fails closed under ``$BRAIN_REQUIRE_REAL_EMBEDDER``.
737
+ """
738
+ if prefer == "hash":
739
+ return HashEmbedder()
740
+ if prefer == "onnx":
741
+ return OnnxEmbedder()
742
+ if prefer == "onnx-int8":
743
+ return OnnxEmbedder(quantization="int8")
744
+ if prefer == "arctic":
745
+ return ArcticEmbedder()
746
+ cat = os.environ.get("BRAIN_EMBED_MODEL")
747
+ if prefer == "catalog":
748
+ if not cat:
749
+ raise EmbedderUnavailable("prefer='catalog' needs $BRAIN_EMBED_MODEL")
750
+ if is_qwen_model(cat):
751
+ return QwenEmbedder(cat)
752
+ return CatalogEmbedder(cat)
753
+ if prefer == "qwen":
754
+ cat = cat or "n24q02m/Qwen3-Embedding-0.6B-ONNX"
755
+ return QwenEmbedder(cat)
756
+ # auto — the default real-semantic path is now direct-ONNX e5-small.
757
+ if cat and is_qwen_model(cat) and QwenEmbedder.available():
758
+ return QwenEmbedder(cat)
759
+ if OnnxEmbedder.available():
760
+ return OnnxEmbedder()
761
+ if cat and CatalogEmbedder.available():
762
+ return CatalogEmbedder(cat)
763
+ if ArcticEmbedder.available():
764
+ return ArcticEmbedder()
765
+ return _implicit_hash_fallback()
766
+
767
+
768
+ def probe_auto_embedder() -> tuple[str, str]:
769
+ """Read-only classification of which embedder the live runtime WOULD use,
770
+ for ``brain doctor``'s liveness probe (DV-03, 2026-07-09). Returns
771
+ ``(state, backend)`` where ``state`` is one of:
772
+
773
+ * ``"real"`` — a real semantic embedder is available;
774
+ * ``"explicit-hash"`` — ``$BRAIN_EMBEDDER=hash`` was chosen deliberately
775
+ (tests/CI) — NOT a failure; must never gate/alarm;
776
+ * ``"implicit-hash"`` — the auto-path found no real embedder and would
777
+ SILENTLY degrade to the non-semantic HashEmbedder
778
+ (the dangerous case — semantic search goes random,
779
+ the exact silent failure DV-03 hardens against).
780
+
781
+ Mirrors the auto-selection in ``get_embedder`` but WITHOUT constructing a
782
+ HashEmbedder or emitting the fallback warning, so it is safe to call from
783
+ the read-only doctor. ponytail: it duplicates the ``.available()`` chain
784
+ rather than calling ``get_embedder`` precisely to avoid that function's
785
+ stderr warning + HashEmbedder construction side effects.
786
+ """
787
+ forced = os.environ.get("BRAIN_EMBEDDER", "auto").strip().lower()
788
+ if forced == "hash":
789
+ return ("explicit-hash", "hash (BRAIN_EMBEDDER=hash)")
790
+ cat = os.environ.get("BRAIN_EMBED_MODEL")
791
+ if forced in ("onnx", "onnx-int8"):
792
+ return ("real", "onnx") if OnnxEmbedder.available() else ("implicit-hash", "onnx-unavailable")
793
+ if forced == "arctic":
794
+ return ("real", "arctic") if ArcticEmbedder.available() else ("implicit-hash", "arctic-unavailable")
795
+ if forced == "qwen":
796
+ return ("real", "qwen") if QwenEmbedder.available() else ("implicit-hash", "qwen-unavailable")
797
+ if forced == "catalog":
798
+ ok = bool(cat) and (QwenEmbedder.available() if (cat and is_qwen_model(cat)) else CatalogEmbedder.available())
799
+ return ("real", "catalog") if ok else ("implicit-hash", "catalog-unavailable")
800
+ # auto (or unset / unrecognised) — mirror get_embedder's discovery order.
801
+ if cat and is_qwen_model(cat) and QwenEmbedder.available():
802
+ return ("real", "qwen")
803
+ if OnnxEmbedder.available():
804
+ return ("real", "onnx")
805
+ if cat and CatalogEmbedder.available():
806
+ return ("real", "catalog")
807
+ if ArcticEmbedder.available():
808
+ return ("real", "arctic")
809
+ return ("implicit-hash", "no-real-embedder")
810
+
811
+
812
+ # Approximate download size for the install/warmup UX hint ONLY (never a perf
813
+ # or capability claim) — the plan's own "~300 MB" figure for the fp32 e5-small
814
+ # ONNX export. Not read anywhere that affects behaviour.
815
+ ONNX_MODEL_SIZE_HINT = "~300 MB"
816
+
817
+
818
+ def model_cache_ready(embedder: "Embedder | None" = None) -> bool | None:
819
+ """Non-network probe (S02/CS-01): are the resolved embedder's model weights
820
+ ALREADY present on disk, i.e. would a warmup/first embed call run offline?
821
+
822
+ Returns ``True`` (cached, ready), ``False`` (would trigger a download —
823
+ pending), or ``None`` when the question doesn't apply (the explicit
824
+ HashEmbedder never downloads anything, or the embedder shape is unknown).
825
+ NEVER downloads and NEVER constructs an ONNX session — safe to call from
826
+ ``brain status`` on every invocation.
827
+ """
828
+ e = embedder if embedder is not None else get_embedder(
829
+ os.environ.get("BRAIN_EMBEDDER", "auto")
830
+ )
831
+ if isinstance(e, HashEmbedder):
832
+ return None
833
+ if not isinstance(e, OnnxEmbedder):
834
+ return None # Arctic/Catalog/Qwen: not the S02 auto-default; not probed
835
+ local_dir = e._local_dir
836
+ onnx_file = e._onnx_file
837
+ if local_dir:
838
+ # Bundled/staged/VM layout (S06/INT-02): files are expected directly
839
+ # on disk, no HF cache semantics.
840
+ direct = os.path.join(local_dir, onnx_file)
841
+ if os.path.exists(direct) and os.path.exists(
842
+ os.path.join(local_dir, "tokenizer.json")
843
+ ):
844
+ return True
845
+ try:
846
+ from huggingface_hub import snapshot_download
847
+ except Exception:
848
+ return None
849
+ pat = [onnx_file, onnx_file + "_data", "tokenizer*", "*.json"]
850
+ try:
851
+ snapshot_download(
852
+ e._hf_repo, cache_dir=local_dir, allow_patterns=pat,
853
+ local_files_only=True,
854
+ )
855
+ return True
856
+ except Exception:
857
+ return False