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/overlay.py ADDED
@@ -0,0 +1,183 @@
1
+ """Per-user personalization overlay (PER-01 / PER-02).
2
+
3
+ The substrate (`vault/brain/`, `vault/raw/`) is generic — it carries NO
4
+ hard-coded owner identity. Brand/voice/keyword/people content that used to be
5
+ wired straight into the kernel is a **data-driven slot** any new owner fills
6
+ with their own: the overlay.
7
+
8
+ Layout (the generic, owner-agnostic shape):
9
+
10
+ overlay/
11
+ ├── voice/ *.md — durable writing voice (tone, register, sign-offs)
12
+ ├── brand/ *.md — naming/anonymisation/title conventions
13
+ ├── keywords/ *.md — glossary / acronym / codename decoder ring
14
+ └── people/ *.md — the always-on people this owner's notes reference
15
+
16
+ Each file carries a small frontmatter block (`overlay_type: <category>`) so a
17
+ validator can check shape without guessing from folder name alone. See
18
+ `overlay/README.md` for the full schema + starter scaffold.
19
+
20
+ This module is intentionally **filesystem-only** — it never constructs a
21
+ `BrainCore` or opens the index. `brain init --validate-overlay` has to work on
22
+ a brand-new install before any index exists, so overlay validation must not
23
+ depend on one.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ import re
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from . import frontmatter
33
+
34
+ CATEGORIES: tuple[str, ...] = ("voice", "brand", "keywords", "people")
35
+
36
+ # AUT-01/AUT-03 (ADR-0003 Ruling c/e): the HTML brief/digest renderers are
37
+ # pure-render — all overlay I/O happens here, once, before the render call.
38
+ # Two OPTIONAL frontmatter keys on a brand/*.md file (alongside the existing
39
+ # overlay_type/title/updated) let an owner brand the generated HTML without
40
+ # any new overlay category or schema ceremony. Absent -> neutral fallback
41
+ # (zero hard-coded owner content is the kernel/overlay contract).
42
+ _DEFAULT_BRAND_TITLE = "Brain Brief"
43
+ _DEFAULT_ACCENT = "#2563eb"
44
+ _HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
45
+
46
+
47
+ def overlay_dir(
48
+ vault: str | os.PathLike[str] | None = None,
49
+ explicit: str | os.PathLike[str] | None = None,
50
+ ) -> Path:
51
+ """Resolve the active overlay directory.
52
+
53
+ Precedence: ``explicit`` arg (``--overlay-dir``) > ``$BRAIN_OVERLAY_DIR`` >
54
+ ``<vault>/overlay`` (the overlay travels with the user's vault, alongside
55
+ ``raw/`` and ``brain/`` — see AGENTS.md §1).
56
+ """
57
+ if explicit:
58
+ return Path(explicit).expanduser().resolve()
59
+ env = os.environ.get("BRAIN_OVERLAY_DIR")
60
+ if env:
61
+ return Path(env).expanduser().resolve()
62
+ from . import config
63
+
64
+ return config.vault_root(vault) / "overlay"
65
+
66
+
67
+ def _validate_category_file(path: Path, category: str) -> list[str]:
68
+ """Return a list of human-readable issues for one overlay file (empty = OK)."""
69
+ issues: list[str] = []
70
+ try:
71
+ text = path.read_text(encoding="utf-8")
72
+ except Exception as exc: # pragma: no cover - unreadable file is rare
73
+ return [f"{path.name}: unreadable ({type(exc).__name__}: {exc})"]
74
+
75
+ meta, body = frontmatter.parse_text(text)
76
+ if not meta:
77
+ issues.append(f"{path.name}: missing or unparseable frontmatter")
78
+ return issues
79
+
80
+ declared = meta.get("overlay_type")
81
+ if declared != category:
82
+ issues.append(
83
+ f"{path.name}: overlay_type={declared!r} does not match its "
84
+ f"directory ({category!r})"
85
+ )
86
+ if not body.strip():
87
+ issues.append(f"{path.name}: frontmatter present but body is empty")
88
+ return issues
89
+
90
+
91
+ def validate_overlay(path: Path) -> dict[str, Any]:
92
+ """Validate an overlay directory's shape. Pure filesystem check.
93
+
94
+ Required shape: ``path`` exists and contains, for EACH of ``CATEGORIES``,
95
+ a subdirectory with at least one ``*.md`` file whose frontmatter declares
96
+ ``overlay_type: <category>``. Returns a report dict (never raises on a
97
+ malformed/missing overlay — that is what ``valid: false`` is for).
98
+ """
99
+ if not path.exists():
100
+ return {
101
+ "overlay_dir": str(path),
102
+ "exists": False,
103
+ "valid": False,
104
+ "categories": {
105
+ c: {"present": False, "file_count": 0, "issues": [f"{c}/ missing (overlay dir does not exist)"]}
106
+ for c in CATEGORIES
107
+ },
108
+ "errors": [f"overlay dir does not exist: {path}"],
109
+ }
110
+
111
+ categories: dict[str, Any] = {}
112
+ errors: list[str] = []
113
+
114
+ for cat in CATEGORIES:
115
+ cat_dir = path / cat
116
+ issues: list[str] = []
117
+ file_count = 0
118
+ present = cat_dir.is_dir()
119
+ if not present:
120
+ issues.append(f"missing category directory: {cat}/")
121
+ else:
122
+ md_files = sorted(cat_dir.glob("*.md"))
123
+ file_count = len(md_files)
124
+ if file_count == 0:
125
+ issues.append(f"{cat}/ exists but has no .md files")
126
+ for f in md_files:
127
+ issues.extend(_validate_category_file(f, cat))
128
+ categories[cat] = {"present": present, "file_count": file_count, "issues": issues}
129
+ errors.extend(f"{cat}: {issue}" for issue in issues)
130
+
131
+ return {
132
+ "overlay_dir": str(path),
133
+ "exists": True,
134
+ "valid": len(errors) == 0,
135
+ "categories": categories,
136
+ "errors": errors,
137
+ }
138
+
139
+
140
+ def resolve_brand(
141
+ vault: str | os.PathLike[str] | None = None,
142
+ explicit: str | os.PathLike[str] | None = None,
143
+ ) -> dict[str, Any]:
144
+ """Resolve brand data for the HTML brief/digest renderers (AUT-01/AUT-03).
145
+
146
+ Reads the FIRST ``overlay/brand/*.md`` file (sorted, deterministic) if
147
+ present and takes its ``title`` plus two optional keys — ``owner_name``,
148
+ ``accent_color`` (a ``#rgb``/``#rrggbb`` hex string; anything else is
149
+ ignored, never trusted into CSS unvalidated). Never raises; a missing
150
+ overlay, empty ``brand/`` category, or unreadable file all fall back to
151
+ the NEUTRAL default below — the renderer must never depend on an owner
152
+ overlay existing.
153
+ """
154
+ result: dict[str, Any] = {
155
+ "present": False,
156
+ "title": _DEFAULT_BRAND_TITLE,
157
+ "owner_name": None,
158
+ "accent_color": _DEFAULT_ACCENT,
159
+ }
160
+ brand_dir = overlay_dir(vault, explicit) / "brand"
161
+ brand_files = sorted(brand_dir.glob("*.md")) if brand_dir.is_dir() else []
162
+ if not brand_files:
163
+ return result
164
+ try:
165
+ text = brand_files[0].read_text(encoding="utf-8")
166
+ except Exception:
167
+ return result
168
+
169
+ meta, _body = frontmatter.parse_text(text)
170
+ if not meta:
171
+ return result
172
+
173
+ title = meta.get("title")
174
+ owner_name = meta.get("owner_name")
175
+ accent = meta.get("accent_color")
176
+ if isinstance(title, str) and title.strip():
177
+ result["title"] = title.strip()
178
+ if isinstance(owner_name, str) and owner_name.strip():
179
+ result["owner_name"] = owner_name.strip()
180
+ if isinstance(accent, str) and _HEX_COLOR_RE.match(accent.strip()):
181
+ result["accent_color"] = accent.strip()
182
+ result["present"] = True
183
+ return result
brain/projection.py ADDED
@@ -0,0 +1,79 @@
1
+ """Workspace projection — the REAL containment control (consensus hardening).
2
+
3
+ The classification filter (brain.classification) only governs what the
4
+ cooperative `brain` CLI *prints*. It is NOT containment: any file-capable
5
+ harness can read the Markdown in ``vault/`` directly and see every tier. The
6
+ plan's consensus hardening requires a real control. We choose option (a):
7
+
8
+ Project a workspace that physically EXCLUDES Restricted/MNPI (and anything
9
+ above the cap, and all unlabelled/default-deny notes) for untrusted harnesses.
10
+
11
+ An untrusted harness (e.g. a Cowork VM leg) is given access ONLY to the
12
+ projected directory, never the full vault. Because the sensitive files are not
13
+ present in the projection, a direct file read on that surface cannot surface
14
+ them — proven by tests/test_direct_file_read.py.
15
+
16
+ This composes with the host/VM trust split (substrate-spec §4): the host owns
17
+ the full vault and the writer role; the VM sees only a projection.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import shutil
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ from . import classification as cls
26
+ from .notes import scan_vault
27
+
28
+
29
+ @dataclass
30
+ class ProjectionResult:
31
+ dest: Path
32
+ max_tier: str
33
+ copied: int
34
+ excluded: int
35
+ excluded_unlabelled: int
36
+
37
+ def to_dict(self) -> dict:
38
+ return {
39
+ "dest": str(self.dest),
40
+ "max_tier": self.max_tier,
41
+ "copied": self.copied,
42
+ "excluded": self.excluded,
43
+ "excluded_unlabelled": self.excluded_unlabelled,
44
+ }
45
+
46
+
47
+ def project_workspace(vault: Path, dest: Path, max_tier: str = cls.DEFAULT_MAX_TIER) -> ProjectionResult:
48
+ """Materialise a filtered copy of ``vault`` into ``dest``.
49
+
50
+ Only notes whose effective classification rank <= ``max_tier`` are copied.
51
+ Unlabelled/unrecognised notes are default-denied (treated as MNPI) and
52
+ excluded unless ``max_tier == "MNPI"``. ``dest`` is recreated from scratch
53
+ each call so it can never retain a previously-projected sensitive file.
54
+ """
55
+ vault = Path(vault)
56
+ dest = Path(dest)
57
+ flt = cls.ClassificationFilter(max_tier=max_tier)
58
+
59
+ if dest.exists():
60
+ shutil.rmtree(dest)
61
+ dest.mkdir(parents=True)
62
+
63
+ copied = excluded = excluded_unlabelled = 0
64
+ for note in scan_vault(vault):
65
+ if flt.allows(note.classification):
66
+ rel = note.path.relative_to(vault)
67
+ out = dest / rel
68
+ out.parent.mkdir(parents=True, exist_ok=True)
69
+ out.write_text(note.path.read_text(encoding="utf-8"), encoding="utf-8")
70
+ copied += 1
71
+ else:
72
+ excluded += 1
73
+ if cls.is_default_denied(note.classification):
74
+ excluded_unlabelled += 1
75
+
76
+ return ProjectionResult(
77
+ dest=dest, max_tier=max_tier, copied=copied,
78
+ excluded=excluded, excluded_unlabelled=excluded_unlabelled,
79
+ )
brain/rerank.py ADDED
@@ -0,0 +1,425 @@
1
+ """Reranker ADAPTER INTERFACE + a real cross-encoder + an identity fallback (RET-02).
2
+
3
+ A reranker is an OPTIONAL precision booster: after the fused hybrid_search
4
+ (``brain.index.BrainIndex.hybrid_search``) produces a coarse top-N, a
5
+ cross-encoder re-scores each (query, passage) pair jointly and re-orders only
6
+ the top 10-20 candidates. It is strictly skippable — every retrieval path runs
7
+ correctly with reranking switched OFF, and degrades to the identity reranker
8
+ (order preserved) whenever the model runtime is unavailable.
9
+
10
+ Design of record: **Alibaba-NLP/gte-multilingual-reranker-base** (Apache-2.0,
11
+ multilingual, ~int8/ONNX). Like the Arctic embedder it is run locally over ONNX
12
+ (no PyTorch) via fastembed's ``TextCrossEncoder``; the model is loaded lazily on
13
+ first ``rerank`` so merely constructing the reranker is cheap and offline.
14
+
15
+ Two implementations satisfy the ``Reranker`` protocol:
16
+
17
+ * ``GteReranker`` — the real cross-encoder via fastembed/ONNX. Raises
18
+ ``RerankerUnavailable`` if the runtime/model is absent.
19
+ * ``NoopReranker`` — identity: returns candidates in their incoming order with
20
+ monotonically-decreasing synthetic scores. Always
21
+ available, network-free; the guaranteed fallback and the
22
+ "rerank skipped" path.
23
+
24
+ Because rerank is bounded to the top 10-20 (``RERANK_TOP_DEFAULT``), its latency
25
+ is comparable to today's single rerank step regardless of corpus size.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ from typing import Protocol, Sequence, runtime_checkable
31
+
32
+
33
+ def _ort_threads() -> int | None:
34
+ """Intra-op thread count for the reranker ONNX session (S11 speed fix).
35
+
36
+ Default: all physical cores. The reranker cross-encodes the top-N
37
+ (query, passage) pairs in ONE batched forward pass, so saturating the
38
+ batch dimension across cores is what keeps query p95 interactive.
39
+ Override via ``$BRAIN_RERANK_THREADS``."""
40
+ raw = os.environ.get("BRAIN_RERANK_THREADS")
41
+ if raw and raw.strip().isdigit():
42
+ return int(raw)
43
+ try:
44
+ return os.cpu_count() or None
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ def _ort_providers() -> list[str]:
50
+ """Execution providers for the reranker (S11 speed fix). Default CPU
51
+ (the path the eval gate ran on); opt into Apple CoreML (ANE/GPU) via
52
+ ``$BRAIN_RERANK_PROVIDERS=CoreMLExecutionProvider``. Comma-separate for
53
+ a fallback chain."""
54
+ raw = os.environ.get("BRAIN_RERANK_PROVIDERS")
55
+ if raw and raw.strip():
56
+ return [p.strip() for p in raw.split(",") if p.strip()]
57
+ return ["CPUExecutionProvider"]
58
+
59
+ # Bound the rerank window: only the coarse top-N is re-scored, never the whole
60
+ # candidate set. Clamped to [RERANK_TOP_MIN, RERANK_TOP_MAX] at call sites.
61
+ #
62
+ # RERANK_TOP_MAX is the latency-vs-recall lever. The original [10,20] band
63
+ # assumed the bi-encoder already had the right doc near the top — TRUE for
64
+ # same-language queries, FALSE for CROSS-LINGUAL ones, where a relevant
65
+ # EN-content note can sit at fused rank 40+ (buried under same-language
66
+ # transcript chunks) and never enter a top-20 rerank window. A wide-candidate
67
+ # cross-encoder pass (top 100–200) is the standard agentic "retrieve broad →
68
+ # rerank" recovery for exactly that case, so the ceiling is env-overridable via
69
+ # BRAIN_RERANK_MAX (default 20 keeps the conservative latency default). The
70
+ # cross-encoder re-scores the full note body, giving brain a whole-note signal
71
+ # at rerank time — the incumbent's structural advantage, recovered post-hoc.
72
+ RERANK_TOP_DEFAULT = 15
73
+ RERANK_TOP_MIN = 10
74
+ RERANK_TOP_MAX = 20
75
+
76
+ # The model of record. The original design named gte-multilingual-reranker-base,
77
+ # but that model is NOT in fastembed's TextCrossEncoder catalog (verified
78
+ # 2026-06-28 — TextCrossEncoder.list_supported_models()), so it cannot run in the
79
+ # chosen ONNX/no-PyTorch runtime. The of-record multilingual cross-encoder is now
80
+ # jina-reranker-v2-base-multilingual (in the fastembed catalog, ~1.1 GB int8/ONNX,
81
+ # CC-BY-NC for the weights — gate at deploy if commercial use is required). It
82
+ # scores cross-lingual (PT-query ↔ EN-passage) pairs correctly with a wide margin
83
+ # where the e5-small bi-encoder cannot. Override via BRAIN_RERANKER_MODEL.
84
+ GTE_RERANKER_MODEL_ID = "jinaai/jina-reranker-v2-base-multilingual"
85
+
86
+
87
+ def _resolve_reranker_model() -> str:
88
+ """The reranker model id, env-overridable via ``BRAIN_RERANKER_MODEL``."""
89
+ return os.environ.get("BRAIN_RERANKER_MODEL") or GTE_RERANKER_MODEL_ID
90
+
91
+
92
+ @runtime_checkable
93
+ class Reranker(Protocol):
94
+ model_id: str
95
+
96
+ def rerank(self, query: str, passages: Sequence[str]) -> list[float]:
97
+ """Return one relevance score per passage (higher = more relevant),
98
+ aligned by index with ``passages``."""
99
+
100
+
101
+ class RerankerUnavailable(RuntimeError):
102
+ """Raised when the requested reranker backend is not importable/usable."""
103
+
104
+
105
+ class NoopReranker:
106
+ """Identity reranker: preserves the incoming order.
107
+
108
+ Emits descending synthetic scores so a stable sort by score is a no-op on the
109
+ incoming order. This is the value returned whenever reranking is skipped or
110
+ the real model runtime is unavailable — callers never special-case it.
111
+ """
112
+
113
+ model_id = "noop"
114
+
115
+ def rerank(self, query: str, passages: Sequence[str]) -> list[float]:
116
+ n = len(passages)
117
+ # Descending, position-preserving scores: n, n-1, ... 1.
118
+ return [float(n - i) for i in range(n)]
119
+
120
+
121
+ class GteReranker:
122
+ """gte-multilingual-reranker-base via fastembed/ONNX — NO PyTorch.
123
+
124
+ Lazy: the ONNX cross-encoder is created on first ``rerank`` so constructing
125
+ the reranker (to read ``model_id``) is cheap and offline. Raises
126
+ ``RerankerUnavailable`` if fastembed's cross-encoder support or the model is
127
+ not importable/available.
128
+ """
129
+
130
+ def __init__(
131
+ self,
132
+ model_id: str | None = None,
133
+ *,
134
+ cache_dir: str | None = None,
135
+ ) -> None:
136
+ self.model_id = model_id or _resolve_reranker_model()
137
+ self._cache_dir = cache_dir or os.environ.get("BRAIN_FASTEMBED_CACHE")
138
+ self._model = None # lazily created TextCrossEncoder
139
+
140
+ @staticmethod
141
+ def available() -> bool:
142
+ try:
143
+ from fastembed.rerank.cross_encoder import TextCrossEncoder # noqa: F401
144
+
145
+ return True
146
+ except Exception:
147
+ return False
148
+
149
+ def _ensure_model(self):
150
+ if self._model is None:
151
+ try:
152
+ from fastembed.rerank.cross_encoder import TextCrossEncoder
153
+ except Exception as exc: # pragma: no cover - exercised when absent
154
+ raise RerankerUnavailable(
155
+ "fastembed cross-encoder support not importable; install the "
156
+ "'embed' extra or bundle the reranker model"
157
+ ) from exc
158
+ try:
159
+ self._model = TextCrossEncoder(
160
+ model_name=self.model_id, cache_dir=self._cache_dir,
161
+ threads=_ort_threads(), providers=_ort_providers(),
162
+ )
163
+ except Exception as exc: # pragma: no cover - model unavailable offline
164
+ raise RerankerUnavailable(
165
+ f"reranker model {self.model_id!r} unavailable: {exc}"
166
+ ) from exc
167
+ return self._model
168
+
169
+ def rerank(self, query: str, passages: Sequence[str]) -> list[float]:
170
+ if not passages:
171
+ return []
172
+ model = self._ensure_model()
173
+ return [float(s) for s in model.rerank(query, list(passages))]
174
+
175
+
176
+ class QwenReranker:
177
+ """Qwen3-Reranker-0.6B via the ``qwen3-embed`` lib (ONNX INT8, Apache-2.0).
178
+
179
+ The 2025-2026 best-in-class small multilingual reranker (MMTEB-R 66.36).
180
+ Shipped in S11 (UPG-03) as the upgrade over ``jina-reranker-v2`` — BOTH a
181
+ quality gain (largest on long multilingual docs, +27 on MLDR per the tech
182
+ report) AND a licence fix (jina-reranker-v2 is CC-BY-NC-4.0, a corporate-
183
+ deployment blocker; Qwen3 is Apache-2.0). ~573 MB INT8 ONNX.
184
+
185
+ Loaded via the same ``qwen3-embed`` lib as the embedder (it exposes a
186
+ ``TextCrossEncoder`` with a fastembed-compatible ``rerank(query, docs)``
187
+ API). Lazy: the ONNX session is created on first ``rerank``.
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ model_id: str | None = None,
193
+ *,
194
+ cache_dir: str | None = None,
195
+ ) -> None:
196
+ self.model_id = model_id or _resolve_reranker_model()
197
+ self._cache_dir = cache_dir or os.environ.get("BRAIN_FASTEMBED_CACHE")
198
+ self._model = None
199
+
200
+ @staticmethod
201
+ def available() -> bool:
202
+ try:
203
+ import qwen3_embed # noqa: F401
204
+ import onnxruntime # noqa: F401
205
+
206
+ return True
207
+ except Exception:
208
+ return False
209
+
210
+ def _ensure_model(self):
211
+ if self._model is None:
212
+ try:
213
+ from qwen3_embed import TextCrossEncoder
214
+ except Exception as exc: # pragma: no cover
215
+ raise RerankerUnavailable(
216
+ "qwen3-embed not importable; pip install qwen3-embed"
217
+ ) from exc
218
+ try:
219
+ self._model = TextCrossEncoder(
220
+ model_name=self.model_id, cache_dir=self._cache_dir,
221
+ threads=_ort_threads(), providers=_ort_providers(),
222
+ )
223
+ except Exception as exc: # pragma: no cover - model unavailable offline
224
+ raise RerankerUnavailable(
225
+ f"reranker model {self.model_id!r} unavailable: {exc}"
226
+ ) from exc
227
+ return self._model
228
+
229
+ def rerank(self, query: str, passages: Sequence[str]) -> list[float]:
230
+ if not passages:
231
+ return []
232
+ model = self._ensure_model()
233
+ return [float(s) for s in model.rerank(query, list(passages))]
234
+
235
+
236
+ def _is_qwen_reranker(model_id: str) -> bool:
237
+ low = (model_id or "").lower()
238
+ return "qwen3-reranker" in low or "qwen-rerank" in low
239
+
240
+
241
+ # --- Fully-open reranker of record (replaces the CC-BY-NC jina-reranker-v2) ---
242
+ # gte-multilingual-reranker-base is Apache-2.0, an XLM-R ENCODER cross-encoder
243
+ # (~306M). It is the best QUALITY among the latency-affordable fully-open
244
+ # multilingual rerankers: an encoder is far faster per pair than the decoder
245
+ # rerankers (Qwen3-Reranker, mxbai-rerank-v2) that fail the HP latency gate, and
246
+ # it is licence-clean (jina-reranker-v2 — the prior model of record — is CC-BY-NC,
247
+ # barred for commercial use in your organization). Loaded DIRECTLY via ONNX Runtime + tokenizers
248
+ # (bge-reranker-v2-m3 / gte are NOT in fastembed's catalogue). Latency ~1.65 s /
249
+ # top-15 query on an M4 ⇒ ~4 s projected on a corporate HP, so reranking stays
250
+ # OPT-IN (the default at the call site remains NoopReranker).
251
+ OPEN_DEFAULT_RERANKER_REPO = "onnx-community/gte-multilingual-reranker-base"
252
+ OPEN_DEFAULT_RERANKER_ONNX = "onnx/model.onnx"
253
+ OPEN_DEFAULT_RERANKER_MODEL_ID = "Alibaba-NLP/gte-multilingual-reranker-base"
254
+
255
+
256
+ class OnnxReranker:
257
+ """Any HuggingFace ONNX cross-encoder reranker loaded DIRECTLY via ONNX
258
+ Runtime — no fastembed-catalog dependency, no PyTorch at runtime.
259
+
260
+ Default-of-record: ``gte-multilingual-reranker-base`` (Apache-2.0). Can also
261
+ load a local exported reranker via ``local_dir`` / ``$BRAIN_RERANKER_ONNX_DIR``
262
+ (e.g. an exported bge-reranker-v2-m3). ENCODER cross-encoders only — do not
263
+ point this at a decoder reranker (Qwen3-Reranker / mxbai-rerank-v2); those
264
+ fail the latency gate on corporate-HP CPU.
265
+ """
266
+
267
+ def __init__(
268
+ self,
269
+ *,
270
+ hf_repo: str | None = None,
271
+ onnx_file: str | None = None,
272
+ local_dir: str | None = None,
273
+ model_id: str | None = None,
274
+ ) -> None:
275
+ self.model_id = model_id or OPEN_DEFAULT_RERANKER_MODEL_ID
276
+ self._hf_repo = hf_repo or OPEN_DEFAULT_RERANKER_REPO
277
+ self._onnx_file = onnx_file or OPEN_DEFAULT_RERANKER_ONNX
278
+ self._local_dir = local_dir or os.environ.get("BRAIN_RERANKER_ONNX_DIR")
279
+ self._sess = None
280
+ self._tok = None
281
+ self._in_names: list[str] | None = None
282
+
283
+ @staticmethod
284
+ def available() -> bool:
285
+ try:
286
+ import onnxruntime # noqa: F401
287
+ import tokenizers # noqa: F401
288
+
289
+ return True
290
+ except Exception:
291
+ return False
292
+
293
+ def _ensure(self):
294
+ if self._sess is None:
295
+ try:
296
+ import onnxruntime as ort
297
+ from tokenizers import Tokenizer
298
+ except Exception as exc: # pragma: no cover
299
+ raise RerankerUnavailable(
300
+ "onnxruntime/tokenizers not importable for OnnxReranker"
301
+ ) from exc
302
+ try:
303
+ if self._local_dir:
304
+ base = self._local_dir
305
+ onnx_path = os.path.join(base, "model_quantized.onnx")
306
+ if not os.path.exists(onnx_path):
307
+ onnx_path = os.path.join(base, "model.onnx")
308
+ else:
309
+ from huggingface_hub import snapshot_download
310
+
311
+ base = snapshot_download(
312
+ self._hf_repo,
313
+ allow_patterns=[
314
+ self._onnx_file,
315
+ self._onnx_file + "_data",
316
+ "tokenizer*",
317
+ "*.json",
318
+ ],
319
+ )
320
+ onnx_path = os.path.join(base, self._onnx_file)
321
+ so = ort.SessionOptions()
322
+ so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
323
+ t = _ort_threads()
324
+ if t:
325
+ so.intra_op_num_threads = t
326
+ self._sess = ort.InferenceSession(
327
+ onnx_path, sess_options=so, providers=_ort_providers()
328
+ )
329
+ self._in_names = [i.name for i in self._sess.get_inputs()]
330
+ self._tok = Tokenizer.from_file(os.path.join(base, "tokenizer.json"))
331
+ # Clamp (query, passage) pairs to the cross-encoder's context
332
+ # window before run(): an over-long pair makes the position-
333
+ # embedding broadcast fail (same crash class as the embedder's
334
+ # "512 by 620"). Passages are already char-capped upstream
335
+ # (_apply_rerank -> [:2000]), so at the default 1024 this
336
+ # truncation effectively never fires for gte/jina and does NOT
337
+ # change the S11-frozen rerank numbers — it is pure crash
338
+ # insurance. Override via $BRAIN_RERANK_MAX_TOKENS.
339
+ _rmax = 1024
340
+ try:
341
+ _rt = os.environ.get("BRAIN_RERANK_MAX_TOKENS")
342
+ if _rt and _rt.strip().isdigit():
343
+ _rmax = int(_rt)
344
+ except Exception:
345
+ pass
346
+ self._tok.enable_truncation(max_length=_rmax)
347
+ except RerankerUnavailable:
348
+ raise
349
+ except Exception as exc: # pragma: no cover - model unavailable offline
350
+ raise RerankerUnavailable(
351
+ f"ONNX reranker {self.model_id!r} unavailable: {exc}"
352
+ ) from exc
353
+ return self._sess
354
+
355
+ def rerank(self, query: str, passages: Sequence[str]) -> list[float]:
356
+ if not passages:
357
+ return []
358
+ self._ensure()
359
+ import numpy as np
360
+
361
+ enc = self._tok.encode_batch([(query, p) for p in passages])
362
+ ii = np.array([e.ids for e in enc], dtype=np.int64)
363
+ am = np.array([e.attention_mask for e in enc], dtype=np.int64)
364
+ feed = {}
365
+ for nm in self._in_names:
366
+ low = nm.lower()
367
+ if "input_id" in low:
368
+ feed[nm] = ii
369
+ elif "attention" in low:
370
+ feed[nm] = am
371
+ elif "token_type" in low:
372
+ feed[nm] = np.zeros_like(ii)
373
+ logits = self._sess.run(None, feed)[0]
374
+ return [float(x[0]) if hasattr(x, "__len__") else float(x) for x in logits]
375
+
376
+
377
+ def get_reranker(prefer: str = "noop") -> Reranker:
378
+ """Adapter selection.
379
+
380
+ ``noop`` forces the identity fallback (the DEFAULT — reranking is OPT-IN at
381
+ the call site, because even the open reranker is ~4 s/query on a corporate
382
+ HP). ``onnx`` selects ``OnnxReranker`` — the fully-open model of record
383
+ (gte-multilingual-reranker-base, Apache-2.0; the CC-BY-NC jina-reranker-v2
384
+ replacement). ``gte`` selects the legacy fastembed ``GteReranker`` (the
385
+ CC-BY-NC jina-v2 catalogue path — AVOID for commercial use).
386
+ ``qwen`` selects ``QwenReranker`` (decoder; fails the HP latency gate —
387
+ legacy only). ``auto`` prefers a Qwen reranker when ``$BRAIN_RERANKER_MODEL``
388
+ names one, else the open OnnxReranker (gte), else the legacy fastembed path,
389
+ else noop.
390
+ """
391
+ env = os.environ.get("BRAIN_RERANKER_PREFER", "").strip().lower()
392
+ if env in {"noop", "gte", "qwen", "onnx"}:
393
+ prefer = env # eval/AB-test override: force a specific reranker
394
+ if prefer == "noop":
395
+ return NoopReranker()
396
+ if prefer == "qwen":
397
+ return QwenReranker()
398
+ if prefer == "gte":
399
+ return GteReranker()
400
+ if prefer == "onnx":
401
+ return OnnxReranker()
402
+ # auto
403
+ rid = _resolve_reranker_model()
404
+ if _is_qwen_reranker(rid) and QwenReranker.available():
405
+ return QwenReranker()
406
+ # OPEN DEFAULT: gte-multilingual-reranker-base via OnnxReranker (Apache-2.0;
407
+ # replaces the CC-BY-NC jina-reranker-v2). Preferred over the fastembed path.
408
+ if OnnxReranker.available():
409
+ return OnnxReranker()
410
+ if GteReranker.available():
411
+ return GteReranker()
412
+ return NoopReranker()
413
+
414
+
415
+ def clamp_rerank_top(n: int) -> int:
416
+ """Clamp the rerank window to [RERANK_TOP_MIN, ceiling], where the ceiling is
417
+ RERANK_TOP_MAX (20) by default but raisable via ``BRAIN_RERANK_MAX`` for the
418
+ wide-candidate cross-encoder pass that recovers cross-lingually buried docs.
419
+ A bad/zero env value falls back to the conservative default."""
420
+ try:
421
+ hi = int(os.environ.get("BRAIN_RERANK_MAX", "") or RERANK_TOP_MAX)
422
+ except ValueError:
423
+ hi = RERANK_TOP_MAX
424
+ hi = max(RERANK_TOP_MAX, hi) # never below the design floor of 20
425
+ return max(RERANK_TOP_MIN, min(hi, n))