rmi-backend/app/services/supabase_rag.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

107 lines
3.3 KiB
Python

#!/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,
}