phi-drift / core /embeddings.py
crexs's picture
Upload folder using huggingface_hub
914e970 verified
Raw
History Blame Contribute Delete
3.14 kB
"""Local and semantic embedding backends for Chroma / DRIFT memory."""
from __future__ import annotations
import hashlib
import math
from typing import Any, List
import threading
import numpy as np
import torch
torch.set_default_device("cpu")
from chromadb.api.types import Documents, Embeddings
class SemanticEmbeddingFunction:
"""sentence-transformers MiniLM vectors (384-dim by default)."""
def __init__(self) -> None:
self._model = None
self._lock = threading.Lock()
def _encoder(self):
with self._lock:
if self._model is None:
from sentence_transformers import SentenceTransformer
import torch
torch.set_default_device("cpu")
self._model = SentenceTransformer("all-MiniLM-L6-v2", device="cpu")
return self._model
@property
def dim(self) -> int:
return int(self._encoder().get_sentence_embedding_dimension())
def name(self) -> str:
return "semantic_minilm"
def embed_query(self, input: str | None = None) -> np.ndarray:
"""Single-string embedding. Parameter name matches Chroma's protocol."""
if input is None:
raise TypeError("embed_query requires a text input")
enc = self._encoder()
v = enc.encode(input, convert_to_numpy=True)
return np.asarray(v, dtype=np.float64)
def embed_documents(self, texts: List[str]) -> List[np.ndarray]:
if not texts:
return []
enc = self._encoder()
batch = enc.encode(texts, convert_to_numpy=True)
return [np.asarray(row, dtype=np.float64) for row in batch]
def __call__(self, input: Documents) -> Embeddings:
raw = self.embed_documents(list(input))
return [e.tolist() for e in raw]
def get_config(self) -> dict[str, Any]:
return {"kind": "semantic_minilm", "dim": self.dim}
@classmethod
def build_from_config(cls, config: dict) -> SemanticEmbeddingFunction:
return cls()
class LocalEmbeddingFunction:
"""Deterministic 64-d hash embedding, L2-normalized (tests / offline fallback)."""
_dim = 64
@staticmethod
def name() -> str:
return "local_hash_embedding"
def embed_query(self, input: str | None = None) -> List[float]:
if input is None:
raise TypeError("embed_query requires a text input")
return self._vec(input)
def embed_documents(self, texts: List[str]) -> List[List[float]]:
return [self._vec(t) for t in texts]
def __call__(self, input: Documents) -> Embeddings:
return self.embed_documents(list(input)) # type: ignore[return-value]
def _vec(self, text: str) -> List[float]:
h = hashlib.sha256(text.encode("utf-8")).digest()
# Stretch 32 bytes to 64 floats in [-1, 1]
raw = (h * 2)[: self._dim]
vals = [((b / 255.0) * 2.0 - 1.0) for b in raw]
mag = math.sqrt(sum(v * v for v in vals)) or 1.0
return [v / mag for v in vals]
def get_default_embedding_function() -> SemanticEmbeddingFunction:
return SemanticEmbeddingFunction()