#!/usr/bin/env python3 """ Supabase vector search helper (RAG integration). Uses the existing search_embeddings RPC already installed on the project. No pinecone/weaviate needed — SQL + pgvector. """ import os from typing import Any import httpx from dotenv import load_dotenv # Load env vars dynamically — override stale Docker env load_dotenv("/app/.env", override=True) def _get_url(): return os.environ.get("SUPABASE_URL", "") def _get_key(): return os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "") or os.environ.get("SUPABASE_SERVICE_KEY", "") def _get_headers(): key = _get_key() return { "apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json", } async def search_similar( query_embedding: list[float], namespace: str = "default", match_count: int = 10, similarity_threshold: float = 0.7, ) -> list[dict[str, Any]]: """ Search for semantically similar documents using pgvector. Returns matching document IDs with similarity scores. Args: query_embedding: The embedding vector from your model namespace: Search namespace to restrict results match_count: Number of results to return similarity_threshold: Minimum cosine similarity (0-1) """ # Pad/truncate to match the pgvector table dimension from app.supabase_vector import TABLE_DIM, pad_vector padded_embedding = pad_vector(query_embedding, TABLE_DIM) url = f"{_get_url()}/rest/v1/rpc/search_embeddings" payload = { "query_embedding": padded_embedding, "match_count": match_count, "namespace": namespace, "similarity_threshold": similarity_threshold, } async with httpx.AsyncClient(timeout=30) as client: r = await client.post(url, json=payload, headers=_get_headers()) if r.status_code == 200: return r.json() return [] async def store_embedding( document_id: str, embedding: list[float], namespace: str = "default", content_hash: str = "", metadata: dict | None = None, model_name: str = "", ) -> dict | None: """ Store an embedding for later retrieval. Idempotent — uses ON CONFLICT (document_id) for upsert via REST. """ url = f"{_get_url()}/rest/v1/embeddings" payload = { "document_id": document_id, "embedding": embedding, "namespace": namespace, "content_hash": content_hash, "metadata": metadata or {}, "model_name": model_name, } headers = dict(_get_headers()) headers["Prefer"] = "resolution=merge-duplicates" async with httpx.AsyncClient(timeout=30) as client: r = await client.post(url, json=payload, headers=headers) return r.json() if r.status_code in (200, 201) else None async def get_namespace_stats(namespace: str = "default") -> dict: """Get document count and stats for a namespace.""" url = f"{_get_url()}/rest/v1/embeddings?namespace=eq.{namespace}&select=id" async with httpx.AsyncClient(timeout=10) as client: r = await client.get(url, headers={**_get_headers(), "Prefer": "count=exact"}) return { "namespace": namespace, "count": int(r.headers.get("content-range", "0").split("/")[-1] or 0), "status": r.status_code, }