qwen_embedding_6 / get_embedding.py
SPAL0074's picture
Upload 10 files
4258647 verified
Raw
History Blame Contribute Delete
4.45 kB
# get_embedding.py
from __future__ import annotations
import asyncio
import os
import yaml
import torch
from typing import List, Optional
from huggingface_hub import snapshot_download
from langchain_huggingface import HuggingFaceEmbeddings
# If you have an auth helper, we keep it
from login import HuggingFaceLogin
class EmbeddingFetcher:
"""
Async-friendly wrapper around a HuggingFace embedding model.
- Lazily initializes the model and downloads repo snapshot at app startup.
- Runs blocking HF / Torch operations in a worker thread via asyncio.to_thread.
- Thread-safe through an asyncio.Lock to prevent duplicate initialization.
"""
def __init__(self, config_path: str = "config.yml") -> None:
self._config_path = config_path
self._ready = False
self._init_lock = asyncio.Lock()
self._model: Optional[HuggingFaceEmbeddings] = None
self._local_model_path: Optional[str] = None
# Authenticate early (likely blocking), but off main thread in ensure_ready()
self._login = HuggingFaceLogin()
# Config defaults (overridden by config.yml)
self._repo_id: str = "SPAL0028/default-model"
self._normalize_embeddings: bool = False
# Device
self._device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# -----------------------
# Public properties
# -----------------------
@property
def model_id(self) -> str:
return self._local_model_path or self._repo_id
@property
def device_str(self) -> str:
return str(self._device)
# -----------------------
# Initialization
# -----------------------
async def ensure_ready(self) -> None:
"""
Idempotent async initializer. Safe to call multiple times.
"""
if self._ready:
return
async with self._init_lock:
if self._ready:
return
# 1) Login (may prompt environment-based auth); keep this off the main loop
await asyncio.to_thread(self._login.authenticate)
# 2) Load config
cfg = await asyncio.to_thread(self._load_config)
self._repo_id = cfg.get("hugging_face_url", self._repo_id)
self._normalize_embeddings = bool(cfg.get("normalize_embeddings", self._normalize_embeddings))
# 3) Download snapshot if needed (blocking IO) off main thread
self._local_model_path = await asyncio.to_thread(
snapshot_download,
self._repo_id
)
# 4) Build embeddings (blocking CPU-bound creation) off main thread
def _build_embeddings():
return HuggingFaceEmbeddings(
model_name=self._local_model_path,
model_kwargs={"device": self._device},
encode_kwargs={"normalize_embeddings": self._normalize_embeddings},
)
self._model = await asyncio.to_thread(_build_embeddings)
self._ready = True
# -----------------------
# Core API
# -----------------------
async def embed(self, texts: List[str] | str) -> List[List[float]]:
"""
Generate embeddings for a list of texts.
Ensures the model is initialized and runs blocking ops using a thread.
"""
await self.ensure_ready()
if isinstance(texts, str):
texts = [texts]
if not texts:
raise ValueError("No texts provided for embedding.")
# Defensive strip to avoid empty items sneaking through
sanitized = [t if isinstance(t, str) else str(t) for t in texts]
sanitized = [t.strip() for t in sanitized if t and t.strip()]
if not sanitized:
raise ValueError("All provided texts are empty after sanitization.")
# langchain_huggingface.HuggingFaceEmbeddings.embed_documents is blocking
vectors = await asyncio.to_thread(self._model.embed_documents, sanitized) # type: ignore[union-attr]
return vectors
# -----------------------
# Helpers
# -----------------------
def _load_config(self) -> dict:
if not os.path.exists(self._config_path):
# Keep behavior predictable if config is missing
return {}
with open(self._config_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}