From bd94facad16943b046d9c742887b08b2122c1053 Mon Sep 17 00:00:00 2001 From: Robert Sigmundsson Date: Thu, 9 Jul 2026 23:23:39 +0200 Subject: [PATCH 1/4] feat(embedding): BGE-M3 HTTP provider (1024D, zero-vec-guard) + factory registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F2 writer A of the Gemini-3072D -> BGE-M3-1024D migration. New BGEM3Embedding posts to a self-hosted /embed service (vast.ai via tunnel), returns 1024D L2-normalized vectors, and RAISES on empty/failed input instead of fabricating a poison zero-vector. Registered in the _create_provider factory + _VALID_PROVIDERS. Shadow only — no prod re-embed. (cherry picked from commit 4607c9af698cbe8dfc83219d409cc986bfb476bb) (cherry picked from commit 4fbe4ad4106f7807076e38d870a99a63bba140ab) (cherry picked from commit 9837606a7775ff99bf6cb51c8a3de534eacbfd18) --- .../engine/embedding/bge_m3_embedding.py | 123 ++++++++++++++++++ src/surreal_memory/engine/embedding/config.py | 1 + .../engine/semantic_discovery.py | 6 + 3 files changed, 130 insertions(+) create mode 100644 src/surreal_memory/engine/embedding/bge_m3_embedding.py diff --git a/src/surreal_memory/engine/embedding/bge_m3_embedding.py b/src/surreal_memory/engine/embedding/bge_m3_embedding.py new file mode 100644 index 00000000..79eebd05 --- /dev/null +++ b/src/surreal_memory/engine/embedding/bge_m3_embedding.py @@ -0,0 +1,123 @@ +"""BGE-M3 embedding provider (HTTP, dense, L2-normalized). + +Talks to a self-hosted BGE-M3 FastAPI service (e.g. on vast.ai reached via an +SSH tunnel at http://127.0.0.1:18100): + + POST {base_url}/embed body {"texts": [...]} header Authorization: Bearer + -> {"embeddings": [[... 1024 floats ...], ...]} (already L2-normalized) + +Zero-vec guard (non-negotiable): on empty/failed input this RAISES rather than +returning a fabricated ``[0.0] * dim`` vector — a zero vector poisons top-k KNN. +Callers (content_worker / smem reindex) treat a raise as "leave neuron pending" +and back-fill on the next cycle. +""" + +from __future__ import annotations + +import os +from typing import Any + +from surreal_memory.engine.embedding.provider import EmbeddingProvider +from surreal_memory.engine.embedding.retry import call_with_retry + +_DEFAULT_MODEL = "bge-m3" +_DEFAULT_DIMENSION = 1024 +_DEFAULT_BASE_URL = "http://127.0.0.1:18100" + + +class _TransientHTTPError(Exception): + """Carries a status_code so retry.is_transient() can classify 429/5xx.""" + + def __init__(self, status_code: int, message: str) -> None: + super().__init__(message) + self.status_code = status_code + + +class BGEM3Embedding(EmbeddingProvider): + """Embedding provider backed by a self-hosted BGE-M3 HTTP service. + + ``httpx`` is imported lazily so the dependency is only needed when this + provider is actually selected. + """ + + def __init__( + self, + model: str = _DEFAULT_MODEL, + api_key: str | None = None, + *, + base_url: str | None = None, + dimension: int | None = None, + timeout: float = 60.0, + base_url_env: str = "SURREAL_MEMORY_EMBEDDING_BASE_URL", + api_key_env: str = "BGE_M3_API_KEY", + ) -> None: + self._model = model + self._base_url = (base_url or os.getenv(base_url_env) or _DEFAULT_BASE_URL).rstrip("/") + self._api_key = ( + api_key or os.getenv(api_key_env) or os.getenv("SURREAL_MEMORY_EMBEDDING_API_KEY") + ) + if not self._api_key: + raise ValueError( + f"A BGE-M3 API key is required. Pass it directly or set the " + f"{api_key_env} environment variable." + ) + env_dim = os.getenv("SURREAL_MEMORY_EMBEDDING_DIMENSION") + resolved = dimension or (int(env_dim) if env_dim and int(env_dim) > 0 else 0) + self._dimension = int(resolved) if resolved else _DEFAULT_DIMENSION + self._timeout = timeout + self._client: Any | None = None + + def _ensure_client(self) -> Any: + if self._client is None: + try: + import httpx + except ImportError as exc: # pragma: no cover + raise ImportError( + "httpx is required for BGEM3Embedding. Install it with: pip install httpx" + ) from exc + self._client = httpx.AsyncClient( + timeout=self._timeout, + headers={"Authorization": f"Bearer {self._api_key}"}, + ) + return self._client + + async def _post_embed(self, texts: list[str]) -> list[list[float]]: + client = self._ensure_client() + + async def _do() -> Any: + resp = await client.post(f"{self._base_url}/embed", json={"texts": texts}) + if resp.status_code in (429, 500, 502, 503, 504): + raise _TransientHTTPError(resp.status_code, f"BGE-M3 {resp.status_code}") + resp.raise_for_status() + return resp.json() + + data = await call_with_retry(_do, provider="BGE-M3") + vecs = data.get("embeddings") if isinstance(data, dict) else None + if not isinstance(vecs, list) or len(vecs) != len(texts): + got = len(vecs) if isinstance(vecs, list) else "?" + raise RuntimeError(f"BGE-M3 returned {got} vectors for {len(texts)} texts") + for v in vecs: + if not isinstance(v, list) or len(v) != self._dimension: + raise RuntimeError( + f"BGE-M3 returned {len(v) if isinstance(v, list) else '?'}D, " + f"expected {self._dimension}D (zero-vec guard: reject mismatched vectors)" + ) + return [list(v) for v in vecs] + + async def embed(self, text: str) -> list[float]: + if not text or not text.strip(): + raise ValueError( + "BGE-M3 embed called with empty text (zero-vec guard: never fabricate)" + ) + return (await self._post_embed([text]))[0] + + async def embed_batch(self, texts: list[str]) -> list[list[float]]: + if not texts: + return [] + if any((not t or not t.strip()) for t in texts): + raise ValueError("BGE-M3 embed_batch received an empty text (zero-vec guard)") + return await self._post_embed(texts) + + @property + def dimension(self) -> int: + return self._dimension diff --git a/src/surreal_memory/engine/embedding/config.py b/src/surreal_memory/engine/embedding/config.py index 1af71e6e..088aabd9 100755 --- a/src/surreal_memory/engine/embedding/config.py +++ b/src/surreal_memory/engine/embedding/config.py @@ -11,6 +11,7 @@ "openrouter", "gemini", "ollama", + "bge_m3", "auto", "", ) diff --git a/src/surreal_memory/engine/semantic_discovery.py b/src/surreal_memory/engine/semantic_discovery.py index 1636d8e6..939ab9a4 100755 --- a/src/surreal_memory/engine/semantic_discovery.py +++ b/src/surreal_memory/engine/semantic_discovery.py @@ -203,6 +203,12 @@ def _create_provider(config: BrainConfig, task_type: str = "RETRIEVAL_QUERY") -> from surreal_memory.engine.embedding.ollama_embedding import OllamaEmbedding provider = OllamaEmbedding(model=model_name) + elif provider_name in ("bge_m3", "bge-m3"): + from surreal_memory.engine.embedding.bge_m3_embedding import BGEM3Embedding + + # base_url / api_key / dimension resolved from env (SURREAL_MEMORY_EMBEDDING_BASE_URL, + # BGE_M3_API_KEY, SURREAL_MEMORY_EMBEDDING_DIMENSION) — see BGEM3Embedding. + provider = BGEM3Embedding(model=model_name or "bge-m3") else: raise ValueError(f"Unknown embedding provider: {provider_name}") From 943d8e0175ccfa7b299d1d32b261d45449508b10 Mon Sep 17 00:00:00 2001 From: Robert Sigmundsson Date: Fri, 10 Jul 2026 08:05:45 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fix(embedding):=20register=20bge=5Fm3=20in?= =?UTF-8?q?=20unified=5Fconfig=20=5FVALID=5FPROVIDERS=20(F2=20gap=20?= =?UTF-8?q?=E2=86=92=20effective-config=20rejected=20it)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BGE-M3 provider was added to engine/embedding/config.py + the factory in F2, but unified_config.py has its OWN provider allow-list that gates the effective embedding config — it still rejected 'bge_m3' ('falling back to disabled'), blocking reindex. Surfaced during F4 pre-cutover verification. (cherry picked from commit 560c424bfa2571ede2c1726e8f7af6a6492c50bb) (cherry picked from commit 75f8e50d1aadd7e1fbe84c77d2a2184d15a118ec) (cherry picked from commit 50df9b255f2a6fd5ed684f55bea64a6bf01b31e5) --- src/surreal_memory/unified_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/surreal_memory/unified_config.py b/src/surreal_memory/unified_config.py index 7cf5c703..175748b0 100755 --- a/src/surreal_memory/unified_config.py +++ b/src/surreal_memory/unified_config.py @@ -131,6 +131,7 @@ class EmbeddingSettings: "openrouter", "gemini", "ollama", + "bge_m3", "auto", "", ) From 76eee41e76100aea5bd2606cd53484867c302756 Mon Sep 17 00:00:00 2001 From: Robert Sigmundsson Date: Sat, 11 Jul 2026 10:45:38 +0200 Subject: [PATCH 3/4] docs(embedding): generic deployment wording in the BGE-M3 provider docstring (cherry picked from commit ad88a53e2fa8b17a3e097b9d4a23420bb5d68e28) (cherry picked from commit 156786a46d6aa813ab619eb38fa6341917d821f7) (cherry picked from commit 0d0b23ece9f0640e4d6b136d4021399452befd5f) --- src/surreal_memory/engine/embedding/bge_m3_embedding.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/surreal_memory/engine/embedding/bge_m3_embedding.py b/src/surreal_memory/engine/embedding/bge_m3_embedding.py index 79eebd05..50997d2c 100644 --- a/src/surreal_memory/engine/embedding/bge_m3_embedding.py +++ b/src/surreal_memory/engine/embedding/bge_m3_embedding.py @@ -1,7 +1,6 @@ """BGE-M3 embedding provider (HTTP, dense, L2-normalized). -Talks to a self-hosted BGE-M3 FastAPI service (e.g. on vast.ai reached via an -SSH tunnel at http://127.0.0.1:18100): +Talks to a self-hosted BGE-M3 FastAPI service exposed on a local endpoint: POST {base_url}/embed body {"texts": [...]} header Authorization: Bearer -> {"embeddings": [[... 1024 floats ...], ...]} (already L2-normalized) From e4aa347323787b2092788e0ec4e7a8499604704c Mon Sep 17 00:00:00 2001 From: Robert Sigmundsson Date: Sat, 18 Jul 2026 15:13:12 +0200 Subject: [PATCH 4/4] fix(embedding): register bge_m3 in capability probe so recall isn't flagged degraded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BGE-M3 HTTP provider was added to the embedding factory (b59be72) and to unified_config._VALID_PROVIDERS (5d2cef7) but NOT to capability._PROVIDER_IMPORT. As a result every smem-mcp/CLI startup logged a spurious "Embedding provider unavailable — running in degraded keyword mode. unknown embedding provider 'bge_m3'", and smem_health / stats_handler reported embeddings as unavailable — even though recall/remember embed correctly via the factory (authed BGE-M3 /embed returns 200). bge_m3 only needs httpx, shipped in the `server` extra. (cherry picked from commit 135a6eced6cc8ee6a278961c1f73d21acbeee11f) --- src/surreal_memory/engine/embedding/capability.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/surreal_memory/engine/embedding/capability.py b/src/surreal_memory/engine/embedding/capability.py index a40aa911..8d95f3e8 100644 --- a/src/surreal_memory/engine/embedding/capability.py +++ b/src/surreal_memory/engine/embedding/capability.py @@ -21,6 +21,11 @@ "openai": ("openai", "embeddings-openai"), "openrouter": ("openai", "embeddings-openrouter"), "sentence_transformer": ("sentence_transformers", "embeddings"), + # HTTP provider (self-hosted BGE-M3): only needs httpx, shipped in the + # ``server`` extra. Without this entry the probe reports the configured + # bge_m3 provider as "unknown" and logs a spurious degraded-keyword-mode + # warning even though recall/remember embed fine via the factory. + "bge_m3": ("httpx", "server"), }