From 251412cb72f5c05fe6544cb5d4567ff3d5705d72 Mon Sep 17 00:00:00 2001 From: Inkithai <108919130+Inkithai@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:01:44 +0000 Subject: [PATCH] fix(vector_store): serialize numpy ndarray embeddings for Supabase jsonb upsert Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- backend/tests/test_qa_self_healing.py | 35 +++++++++++++++++++++++++++ backend/vector_store.py | 15 +++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_qa_self_healing.py b/backend/tests/test_qa_self_healing.py index aa5a879..3a5f788 100644 --- a/backend/tests/test_qa_self_healing.py +++ b/backend/tests/test_qa_self_healing.py @@ -207,6 +207,41 @@ def select(self, *cols, **kw): importlib.reload(vector_store) # restore chroma default for other tests +def test_supabase_upsert_handles_numpy_arrays(): + """Ensure numpy ndarray embeddings do not cause TypeError when upserting to Supabase.""" + os.environ["VECTOR_STORE"] = "supabase" + import importlib + importlib.reload(vector_store) + + import numpy as np + + class FakeSupabaseTable: + def __init__(self): + self.rows = {} + def upsert(self, row, on_conflict=None): + self.rows[row["id"]] = row + return self + def execute(self): + return types.SimpleNamespace(data=list(self.rows.values()), count=len(self.rows)) + + fake_table = FakeSupabaseTable() + + try: + with mock.patch("db._get_client", lambda: mock.Mock(table=lambda name: fake_table)): + emb_array = np.array([0.1, 0.2, 0.3]) + vector_store.upsert( + patient_key="anon_np", + ids=["id1"], + embeddings=[emb_array], + documents=["some doc"], + metadatas=[{"source": "test"}] + ) + assert fake_table.rows["id1"]["embedding"] == [0.1, 0.2, 0.3] + finally: + os.environ.pop("VECTOR_STORE", None) + importlib.reload(vector_store) + + if __name__ == "__main__": fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for fn in fns: diff --git a/backend/vector_store.py b/backend/vector_store.py index 7a7c664..275d0d4 100644 --- a/backend/vector_store.py +++ b/backend/vector_store.py @@ -151,16 +151,21 @@ def _cosine(a: List[float], b: List[float]) -> float: return -1.0 return dot / (na * nb) -def _supabase_upsert(patient_key: str, ids: List[str], embeddings: List[List[float]], documents: List[str], metadatas: List[Dict[str, Any]]): +def _supabase_upsert(patient_key: str, ids: List[str], embeddings: List[Any], documents: List[str], metadatas: List[Dict[str, Any]]): client = _supabase_client() # Upsert per chunk. Supabase doesn't have bulk upsert with jsonb vector easily, # so we do per-row upsert. Chunk counts are small (20-30), so fine. for i, cid in enumerate(ids): + emb = embeddings[i] + if hasattr(emb, "tolist"): + emb = emb.tolist() + elif not isinstance(emb, list): + emb = list(emb) row = { "id": cid, "patient_key": patient_key, "text": documents[i], - "embedding": embeddings[i], # stored as jsonb array + "embedding": emb, # stored as jsonb array "metadata": metadatas[i], } # postgrest upsert on conflict (id) @@ -176,7 +181,11 @@ def _supabase_upsert(patient_key: str, ids: List[str], embeddings: List[List[flo raise logger.info("Supabase upsert %d chunks for %s", len(ids), patient_key) -def _supabase_query(patient_key: str, query_embedding: List[float], n_results: int) -> Tuple[List[str], List[str], List[Dict[str, Any]]]: +def _supabase_query(patient_key: str, query_embedding: Any, n_results: int) -> Tuple[List[str], List[str], List[Dict[str, Any]]]: + if hasattr(query_embedding, "tolist"): + query_embedding = query_embedding.tolist() + elif not isinstance(query_embedding, list): + query_embedding = list(query_embedding) client = _supabase_client() try: res = client.table("chunks").select("id, text, metadata, embedding").eq("patient_key", patient_key).execute()