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.
- brain/__init__.py +39 -0
- brain/__main__.py +14 -0
- brain/_assets/AGENTS.md +564 -0
- brain/_assets/overlay/template/brand/brand-guide.md +24 -0
- brain/_assets/overlay/template/keywords/glossary.md +15 -0
- brain/_assets/overlay/template/people/roster.md +14 -0
- brain/_assets/overlay/template/voice/voice-profile.md +34 -0
- brain/_assets/routines/manifest.json +257 -0
- brain/_assets/scripts/brain-brief-mac.plist +59 -0
- brain/_assets/scripts/brain-brief.sh +74 -0
- brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
- brain/_assets/scripts/brain-synthesis.sh +214 -0
- brain/_assets/scripts/install-brief-mac.sh +152 -0
- brain/_assets/scripts/install-brief-windows.ps1 +97 -0
- brain/_assets/scripts/register_tasks.py +386 -0
- brain/_assets/templates/company.md +21 -0
- brain/_assets/templates/concept.md +27 -0
- brain/_assets/templates/daily.md +20 -0
- brain/_assets/templates/decision.md +42 -0
- brain/_assets/templates/meeting.md +33 -0
- brain/_assets/templates/person.md +20 -0
- brain/_assets/templates/project.md +23 -0
- brain/_assets/templates/state-moc.md +40 -0
- brain/_version.py +12 -0
- brain/anchor.py +121 -0
- brain/audit.py +422 -0
- brain/backup.py +210 -0
- brain/brief.py +417 -0
- brain/capture.py +117 -0
- brain/chunk.py +249 -0
- brain/classification.py +134 -0
- brain/cli.py +1906 -0
- brain/config.py +368 -0
- brain/connect.py +362 -0
- brain/context.py +108 -0
- brain/core.py +3018 -0
- brain/doctor.py +1161 -0
- brain/egress.py +148 -0
- brain/embed.py +857 -0
- brain/encryption.py +217 -0
- brain/frontmatter.py +102 -0
- brain/golden_probe.py +678 -0
- brain/graph.py +369 -0
- brain/graphify.py +352 -0
- brain/index.py +1576 -0
- brain/ingest/__init__.py +19 -0
- brain/ingest/handlers/__init__.py +43 -0
- brain/ingest/handlers/base.py +95 -0
- brain/ingest/handlers/docx.py +78 -0
- brain/ingest/handlers/email.py +228 -0
- brain/ingest/handlers/html.py +142 -0
- brain/ingest/handlers/image.py +91 -0
- brain/ingest/handlers/pdf.py +99 -0
- brain/ingest/handlers/pptx.py +69 -0
- brain/ingest/handlers/tables.py +41 -0
- brain/ingest/handlers/text.py +43 -0
- brain/ingest/handlers/xlsx.py +100 -0
- brain/ingest/handlers/zip.py +163 -0
- brain/ingest/pipeline.py +839 -0
- brain/ingest/transcript.py +158 -0
- brain/init.py +870 -0
- brain/maintenance.py +2266 -0
- brain/mcp_adapter.py +217 -0
- brain/multihop.py +232 -0
- brain/notes.py +195 -0
- brain/overlay.py +183 -0
- brain/projection.py +79 -0
- brain/rerank.py +425 -0
- brain/snapshot.py +231 -0
- brain/update.py +743 -0
- brain/vectors.py +225 -0
- brainiac_cli-0.16.0.dist-info/METADATA +306 -0
- brainiac_cli-0.16.0.dist-info/RECORD +77 -0
- brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
- brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
- brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
- 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))
|