# retoor from __future__ import annotations import hashlib import logging import math import re import time from dataclasses import dataclass, field from devplacepy.config import INTERNAL_EMBED_MODEL, INTERNAL_EMBED_URL from devplacepy.services.deepsearch.llm import gateway_client logger = logging.getLogger(__name__) EMBED_TIMEOUT_SECONDS = 60.0 LOCAL_EMBED_DIMS = 256 EMBED_CACHE_MAX = 5000 TOKEN_PATTERN = re.compile(r"[a-z0-9]+") @dataclass class EmbedResult: vectors: list[list[float]] backend: str latency_ms: int = 0 cache_hits: int = 0 @property def dims(self) -> int: for vector in self.vectors: if vector: return len(vector) return 0 @dataclass class EmbeddingCache: store: dict[str, list[float]] = field(default_factory=dict) max_entries: int = EMBED_CACHE_MAX def key(self, text: str) -> str: return hashlib.sha1((text or "").encode("utf-8")).hexdigest() def get(self, text: str) -> list[float] | None: return self.store.get(self.key(text)) def put(self, text: str, vector: list[float]) -> None: if not vector: return if len(self.store) >= self.max_entries: self.store.pop(next(iter(self.store)), None) self.store[self.key(text)] = vector def _local_vector(text: str) -> list[float]: bucket = [0.0] * LOCAL_EMBED_DIMS tokens = TOKEN_PATTERN.findall((text or "").lower()) if not tokens: return bucket for token in tokens: digest = hashlib.sha1(token.encode("utf-8")).digest() index = int.from_bytes(digest[:4], "big") % LOCAL_EMBED_DIMS sign = 1.0 if digest[4] % 2 == 0 else -1.0 bucket[index] += sign norm = math.sqrt(sum(value * value for value in bucket)) if norm == 0.0: return bucket return [value / norm for value in bucket] def local_embed(texts: list[str]) -> EmbedResult: start = time.monotonic() vectors = [_local_vector(text) for text in texts] latency_ms = int((time.monotonic() - start) * 1000) return EmbedResult(vectors=vectors, backend="local", latency_ms=latency_ms) async def embed_texts( texts: list[str], api_key: str, *, gateway_url: str = INTERNAL_EMBED_URL, cache: EmbeddingCache | None = None, ) -> EmbedResult: if not texts: return EmbedResult(vectors=[], backend="empty") cache_hits = 0 pending_index: list[int] = [] pending_text: list[str] = [] resolved: list[list[float] | None] = [None] * len(texts) if cache is not None: for index, text in enumerate(texts): cached_vector = cache.get(text) if cached_vector is not None: resolved[index] = cached_vector cache_hits += 1 else: pending_index.append(index) pending_text.append(text) else: pending_index = list(range(len(texts))) pending_text = list(texts) if not pending_text: return EmbedResult( vectors=[vector or [] for vector in resolved], backend="cache", latency_ms=0, cache_hits=cache_hits, ) headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "X-App-Reference": "devplace-deepsearch-v-1-0-0", } payload = {"model": INTERNAL_EMBED_MODEL, "input": pending_text} start = time.monotonic() backend = "gateway" try: response = await gateway_client().post( gateway_url, json=payload, headers=headers, timeout=EMBED_TIMEOUT_SECONDS ) if response.status_code >= 400: raise RuntimeError(f"embed gateway returned {response.status_code}") data = response.json() rows = data.get("data") or [] fresh = [row.get("embedding") or [] for row in rows] if len(fresh) != len(pending_text) or any(not vector for vector in fresh): raise RuntimeError("embed gateway returned an incomplete response") except Exception as exc: logger.warning("deepsearch embedding gateway failed, using local: %s", exc) fresh = [_local_vector(text) for text in pending_text] backend = "local" latency_ms = int((time.monotonic() - start) * 1000) for offset, index in enumerate(pending_index): vector = fresh[offset] resolved[index] = vector if cache is not None: cache.put(pending_text[offset], vector) return EmbedResult( vectors=[vector or [] for vector in resolved], backend=backend, latency_ms=latency_ms, cache_hits=cache_hits, )