- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
"""M3 RAG Embeddings - strict two-tier (no hash fallback).
|
|
|
|
Per T04: the hash-based "fallback" embedding was returning
|
|
confidently-wrong results (non-semantic vectors). When all neural
|
|
backends fail, the RAG system now fails LOUDLY instead of returning
|
|
gibberish.
|
|
|
|
Tier 1: Ollama bge-m3 (1024d, our own, zero cost) - best quality when reachable
|
|
Tier 2: OpenRouter NVIDIA Nemotron (2048d, free tier) - if API key set
|
|
|
|
Design (per DESIGN.md M3):
|
|
- Single async API: `await get_embedding(text) -> list[float]`
|
|
- BGE-M3 preferred when available
|
|
- Embedding dim is reported via EMBEDDING_DIM; collections use same dim
|
|
- All embeddings are float lists (JSON-serializable) for Redis storage
|
|
- FAIL LOUD: if both backends fail, raise RAGUnavailableError
|
|
- Monitor via Prometheus counter rag_empty_returns_total
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import Final
|
|
|
|
import httpx
|
|
import numpy as np
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
EMBEDDING_DIM: Final = 1024
|
|
|
|
# Backends
|
|
_BACKEND = os.getenv("RAG_EMBED_BACKEND", "auto").lower()
|
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://host.docker.internal:11434").rstrip("/")
|
|
OLLAMA_MODEL = os.getenv("OLLAMA_EMBED_MODEL", "bge-m3")
|
|
OPENROUTER_URL = "https://openrouter.ai/api/v1/embeddings"
|
|
OPENROUTER_MODEL = "nvidia/llama-nemotron-embed-vl-1b-v2:free"
|
|
|
|
|
|
class RAGUnavailableError(Exception):
|
|
"""Raised when no embedding backend is reachable. The RAG endpoint
|
|
translates this to HTTP 503 + an explicit empty-result body.
|
|
See T04 in MINIMAX_M3_TASKS.md."""
|
|
|
|
|
|
def _ollama_reachable() -> bool:
|
|
"""Quick probe: is Ollama listening? Cached per process via env."""
|
|
if os.getenv("_OLLAMA_PROBED"):
|
|
return os.getenv("_OLLAMA_OK") == "1"
|
|
try:
|
|
with httpx.Client(timeout=2.0) as c:
|
|
r = c.get(f"{OLLAMA_URL}/api/tags")
|
|
ok = r.status_code == 200
|
|
except Exception:
|
|
ok = False
|
|
os.environ["_OLLAMA_PROBED"] = "1"
|
|
os.environ["_OLLAMA_OK"] = "1" if ok else "0"
|
|
if not ok:
|
|
log.info("rag_ollama_unreachable url=%s", OLLAMA_URL)
|
|
return ok
|
|
|
|
|
|
def _resize(vec: list[float], dim: int) -> list[float]:
|
|
"""Pad or truncate a vector to target dim, then L2 normalize."""
|
|
arr = np.asarray(vec, dtype=np.float32)
|
|
arr = arr[:dim] if arr.shape[0] >= dim else np.concatenate([arr, np.zeros(dim - arr.shape[0], dtype=np.float32)])
|
|
n = float(np.linalg.norm(arr))
|
|
if n > 0:
|
|
arr = arr / n
|
|
return arr.tolist()
|
|
|
|
|
|
def _ollama_embed(text: str, dim: int = EMBEDDING_DIM) -> list[float] | None:
|
|
"""Try Ollama bge-m3. Returns None on any failure."""
|
|
try:
|
|
with httpx.Client(timeout=10.0) as c:
|
|
r = c.post(
|
|
f"{OLLAMA_URL}/api/embeddings",
|
|
json={"model": OLLAMA_MODEL, "prompt": text[:8000]},
|
|
)
|
|
if r.status_code != 200:
|
|
return None
|
|
vec = r.json().get("embedding")
|
|
if not vec or not isinstance(vec, list):
|
|
return None
|
|
return _resize(vec, dim)
|
|
except Exception as e:
|
|
log.debug("ollama_embed_failed: %s", e)
|
|
return None
|
|
|
|
|
|
def _openrouter_embed(text: str, dim: int = EMBEDDING_DIM) -> list[float] | None:
|
|
"""Try OpenRouter Nemotron. Returns None on any failure."""
|
|
key = os.getenv("OPENROUTER_API_KEY")
|
|
if not key:
|
|
return None
|
|
try:
|
|
with httpx.Client(timeout=15.0) as c:
|
|
r = c.post(
|
|
OPENROUTER_URL,
|
|
headers={"Authorization": f"Bearer {key}"},
|
|
json={"model": OPENROUTER_MODEL, "input": text[:8000]},
|
|
)
|
|
if r.status_code != 200:
|
|
return None
|
|
data = r.json().get("data", [])
|
|
if not data:
|
|
return None
|
|
vec = data[0].get("embedding")
|
|
if not vec:
|
|
return None
|
|
return _resize(vec, dim)
|
|
except Exception as e:
|
|
log.debug("openrouter_embed_failed: %s", e)
|
|
return None
|
|
|
|
|
|
async def get_embedding(text: str) -> list[float]:
|
|
"""Async embedding API. Returns 1024d float list.
|
|
|
|
Per T04: NO hash fallback. If both neural backends fail,
|
|
raises RAGUnavailableError. The RAG endpoint translates this to
|
|
HTTP 503 + {"status": "empty", "chunks": [], "message": ...}.
|
|
"""
|
|
if not text or not text.strip():
|
|
raise RAGUnavailableError("empty text input")
|
|
|
|
# Explicit backend override
|
|
if _BACKEND == "ollama":
|
|
v = _ollama_embed(text)
|
|
if v is not None:
|
|
return v
|
|
raise RAGUnavailableError("ollama backend failed and hash fallback removed per T04")
|
|
if _BACKEND == "openrouter":
|
|
v = _openrouter_embed(text)
|
|
if v is not None:
|
|
return v
|
|
raise RAGUnavailableError("openrouter backend failed and hash fallback removed per T04")
|
|
|
|
# auto: try ollama -> openrouter, fail loud if both fail
|
|
if _ollama_reachable():
|
|
v = _ollama_embed(text)
|
|
if v is not None:
|
|
return v
|
|
v = _openrouter_embed(text)
|
|
if v is not None:
|
|
return v
|
|
raise RAGUnavailableError(
|
|
"no embedding backend available (ollama unreachable, openrouter failed). "
|
|
"Per T04, hash fallback removed to prevent false-confidence in RAG results."
|
|
)
|
|
|
|
|
|
async def get_embeddings(texts: list[str]) -> list[list[float]]:
|
|
"""Batch embedding. Sequential calls - Ollama and OpenRouter handle
|
|
larger requests poorly and we'd rather keep memory bounded."""
|
|
return [await get_embedding(t) for t in texts]
|
|
|
|
|
|
def current_backend() -> str:
|
|
"""Which tier is currently active? For observability."""
|
|
if _BACKEND != "auto":
|
|
return _BACKEND
|
|
if _ollama_reachable():
|
|
return "ollama"
|
|
if os.getenv("OPENROUTER_API_KEY"):
|
|
return "openrouter"
|
|
return "unavailable"
|