"""Embedding generation for semantic search via pgvector. Priority: 1. AWS Bedrock Titan Embed V2 (direct, best quality, 1024 dims) 2. LiteLLM proxy with configured LITELLM_EMBEDDING_MODEL (fallback) """ import logging from app.config import settings logger = logging.getLogger(__name__) 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 (best quality) ──────────────────────── if settings.LITELLM_EMBEDDING_MODEL and settings.LITELLM_API_KEY: try: import litellm resp = litellm.embedding( model=settings.LITELLM_EMBEDDING_MODEL, input=[clean], api_key=settings.LITELLM_API_KEY, api_base=settings.LITELLM_API_BASE or None, ) emb = resp.data[0]["embedding"] if len(emb) == settings.EMBEDDING_DIMENSIONS: return emb # Dimension mismatch — don't store incompatible vector 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