Spaces:
Runtime error
Runtime error
| """ | |
| packages/memory/embedder.py | |
| Batch embedding layer using sentence-transformers all-MiniLM-L6-v2. | |
| 384-dim cosine-space vectors. CPU-only (HF Spaces free tier). | |
| Design: | |
| - Lazy model load: model downloaded/loaded on first encode() call, not at import. | |
| - Async-safe: encode() runs in executor to avoid blocking the event loop. | |
| - Batch: encode list[str] β list[list[float]] in one model.encode() call. | |
| - Normalise: L2-norm so cosine sim == dot product (Zilliz COSINE metric). | |
| Pre-registered gut-feel bugs (other files that could break this one): | |
| E1 [HIGH] raptor.py calls encode() inside asyncio.gather() across multiple | |
| user_ids simultaneously. sentence-transformers model.encode() is | |
| NOT thread-safe when batch_size > 1. Fix: asyncio.Lock on encode(). | |
| E2 [MED] HF Space free tier has ~2GB RAM. all-MiniLM-L6-v2 uses ~90MB. | |
| If browser_agent.py Playwright is also loaded, OOM risk. | |
| Fix: track peak RAM at startup, warn via /health endpoint. | |
| E3 [LOW] model.encode() returns numpy arrays. ZillizStore expects list[float]. | |
| Implicit .tolist() missing β pymilvus TypeError on upsert. | |
| Fix: explicit .tolist() in encode() return. | |
| """ | |
| import asyncio | |
| import logging | |
| from functools import lru_cache | |
| from typing import List | |
| logger = logging.getLogger(__name__) | |
| MODEL_NAME = "all-MiniLM-L6-v2" | |
| EMBED_DIM = 384 | |
| def _load_model(): | |
| """Load model once, cache in process. Called inside executor thread.""" | |
| from sentence_transformers import SentenceTransformer # type: ignore | |
| logger.info(f"Loading embedding model: {MODEL_NAME}") | |
| model = SentenceTransformer(MODEL_NAME) | |
| return model | |
| class Embedder: | |
| """ | |
| Async batch embedder. | |
| Usage: | |
| embedder = Embedder() | |
| vecs = await embedder.encode(["hello world", "foo bar"]) | |
| # vecs: List[List[float]], shape (2, 384) | |
| """ | |
| def __init__(self): | |
| self._lock = asyncio.Lock() # E1 guard: single encode at a time | |
| self._loop: asyncio.AbstractEventLoop | None = None | |
| async def encode(self, texts: List[str]) -> List[List[float]]: | |
| """ | |
| Encode a batch of texts to 384-dim normalised float vectors. | |
| Args: | |
| texts: Non-empty list of strings. | |
| Returns: | |
| List of float lists, one per input text. | |
| Raises: | |
| ValueError: If texts is empty. | |
| """ | |
| if not texts: | |
| raise ValueError("encode() called with empty texts list") | |
| async with self._lock: # E1: serialise concurrent encode calls | |
| loop = asyncio.get_running_loop() | |
| # Run CPU-bound model.encode in thread pool β avoids blocking event loop | |
| embeddings = await loop.run_in_executor( | |
| None, self._sync_encode, texts | |
| ) | |
| return embeddings | |
| def _sync_encode(self, texts: List[str]) -> List[List[float]]: | |
| """Blocking encode β called inside executor thread.""" | |
| model = _load_model() | |
| # normalize_embeddings=True β L2 norm β cosine sim == dot product | |
| vecs = model.encode( | |
| texts, | |
| batch_size=64, | |
| normalize_embeddings=True, | |
| show_progress_bar=False, | |
| ) | |
| return [v.tolist() for v in vecs] # E3: explicit .tolist() | |
| async def encode_one(self, text: str) -> List[float]: | |
| """Convenience: encode a single string.""" | |
| results = await self.encode([text]) | |
| return results[0] | |
| def dim(self) -> int: | |
| return EMBED_DIM | |