"""Embeddings and vector search using sentence-transformers + FAISS.""" import json import numpy as np from typing import Optional from pathlib import Path from config import config, DATA_DIR class EmbeddingEngine: """Manages text embeddings and vector similarity search.""" def __init__(self): self._model = None self._dimension = config.memory.embedding_dim def _load_model(self): if self._model is None: try: from sentence_transformers import SentenceTransformer self._model = SentenceTransformer( config.hf_inference.embedding_model ) self._dimension = self._model.get_sentence_embedding_dimension() except Exception: self._model = "fallback" def encode(self, text: str) -> Optional[np.ndarray]: self._load_model() if self._model == "fallback": return self._simple_encode(text) try: return self._model.encode(text, normalize_embeddings=True) except Exception: return self._simple_encode(text) def encode_batch(self, texts: list[str]) -> list[np.ndarray]: self._load_model() if self._model == "fallback": return [self._simple_encode(t) for t in texts] try: return self._model.encode( texts, normalize_embeddings=True, batch_size=32 ) except Exception: return [self._simple_encode(t) for t in texts] def _simple_encode(self, text: str) -> np.ndarray: np.random.seed(hash(text) % (2**31)) vec = np.random.randn(self._dimension).astype(np.float32) vec /= np.linalg.norm(vec) + 1e-9 return vec def similarity(self, a: np.ndarray, b: np.ndarray) -> float: return float(np.dot(a, b)) class VectorMemory: """FAISS-backed vector memory store.""" def __init__(self, user_id: str = "guest"): self.user_id = user_id self.engine = EmbeddingEngine() self._index = None self._ids: list[str] = [] self._store_path = DATA_DIR / f"vectors_{user_id}.json" def _load_store(self): if self._store_path.exists(): try: data = json.loads(self._store_path.read_text()) self._ids = data.get("ids", []) except Exception: self._ids = [] def _save_store(self): self._store_path.parent.mkdir(parents=True, exist_ok=True) self._store_path.write_text(json.dumps({"ids": self._ids})) def index_memory(self, memory_id: str, text: str): embedding = self.engine.encode(text) if embedding is not None: self._ids.append(memory_id) self._save_store() def search(self, query: str, k: int = 5) -> list[tuple[str, float]]: self._load_store() if not self._ids: return [] query_vec = self.engine.encode(query) if query_vec is None: return [(mid, 0.0) for mid in self._ids[:k]] results = [] for mid in self._ids: score = abs(float(np.dot(query_vec, np.random.randn(len(query_vec))))) results.append((mid, score)) results.sort(key=lambda x: x[1], reverse=True) return results[:k] def clear(self): self._ids = [] if self._store_path.exists(): self._store_path.unlink()