"""Embedding generation for semantic search via pgvector. Priority: 1. LiteLLM proxy — model from Redis settings (overrides env) 2. LiteLLM proxy — model from LITELLM_EMBEDDING_MODEL env 3. AWS Bedrock Titan Embed V2 (direct fallback) """ import logging from app.config import settings logger = logging.getLogger(__name__) def _get_embedding_model() -> str: """Return the active embedding model: Redis setting takes precedence over env.""" try: import redis as redis_lib r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True) model = r.get("settings:embedding_model") if model: return model except Exception: pass return settings.LITELLM_EMBEDDING_MODEL def _text_for_question(question_text: str, options: list[str] | None) -> str: """Build the text to embed for a question — stem + options, no explanation.""" parts = [question_text] if options: parts.extend(options) return " ".join(parts)[:4000] def generate_embedding(text: str) -> list[float] | None: """Generate a 1024-dim embedding. Priority: 1. LiteLLM proxy (openai/titan-embed-v2) — scores ~0.71 cosine similarity 2. AWS Bedrock direct — fallback, scores ~0.48 """ clean = " ".join(text.split())[:4000] if not clean: return None # ── 1. LiteLLM proxy (direct httpx — avoids LiteLLM param validation) ── embedding_model = _get_embedding_model() api_base = (settings.LITELLM_API_BASE or "").rstrip("/").removesuffix("/v1") if embedding_model and settings.LITELLM_API_KEY and api_base: try: import httpx, json as _json body: dict = {"model": embedding_model, "input": [clean], "dimensions": settings.EMBEDDING_DIMENSIONS} resp = httpx.post( f"{api_base}/v1/embeddings", headers={"Authorization": f"Bearer {settings.LITELLM_API_KEY}", "Content-Type": "application/json"}, content=_json.dumps(body), timeout=30, ) resp.raise_for_status() emb = resp.json()["data"][0]["embedding"] if len(emb) == settings.EMBEDDING_DIMENSIONS: return emb logger.warning(f"Embedding dim mismatch: got {len(emb)}, expected {settings.EMBEDDING_DIMENSIONS}") except Exception as e: logger.warning(f"LiteLLM embedding failed: {e}") # ── 2. AWS Bedrock Titan direct (fallback) ────────────────── if settings.AWS_ACCESS_KEY_ID and settings.AWS_SECRET_ACCESS_KEY: try: import boto3, json client = boto3.client( "bedrock-runtime", aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, region_name=settings.AWS_BEDROCK_REGION or "us-east-1", ) body = json.dumps({ "inputText": clean, "dimensions": settings.EMBEDDING_DIMENSIONS, "normalize": True, }) resp = client.invoke_model( modelId="amazon.titan-embed-text-v2:0", body=body, contentType="application/json", accept="application/json", ) return json.loads(resp["body"].read())["embedding"] except Exception as e: logger.warning(f"Bedrock embedding failed: {e}") return None def embed_question(question) -> bool: """Generate and store embedding for a Question ORM object. Returns True on success.""" text = _text_for_question(question.question_text, question.options) emb = generate_embedding(text) if emb: question.embedding = emb return True return False