From cd30e7ba7e31bc5d7dd99b6d7068aef1470c371d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=ED=9C=98?= Date: Mon, 22 Jun 2026 13:33:01 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(ai):=20=EB=B2=95=EB=A5=A0=20RAG=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=20=EA=B5=AC=ED=98=84=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-ai/.env.example | 2 + backend-ai/app/clients/embedding_client.py | 45 +++++++ backend-ai/app/clients/supabase_client.py | 50 +++++--- backend-ai/app/core/config.py | 5 + backend-ai/app/graph/nodes/legal_rag.py | 2 +- backend-ai/app/rag/retriever.py | 50 +++++++- backend-ai/tests/test_agent_chat.py | 29 ++++- backend-ai/tests/test_legal_retriever.py | 120 ++++++++++++++++++ .../phase3-legal-rag-retriever.md | 27 ++++ 9 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 backend-ai/app/clients/embedding_client.py create mode 100644 backend-ai/tests/test_legal_retriever.py create mode 100644 phases/ai-legal-rag/phase3-legal-rag-retriever.md diff --git a/backend-ai/.env.example b/backend-ai/.env.example index 4030c5f..897cd2e 100644 --- a/backend-ai/.env.example +++ b/backend-ai/.env.example @@ -5,6 +5,8 @@ SUPABASE_DB_URL=postgresql://user:password@host:5432/postgres SUPABASE_SERVICE_ROLE_KEY= GMS_API_KEY= LLM_MODEL= +EMBEDDING_API_KEY= +EMBEDDING_BASE_URL=https://api.openai.com/v1 EMBEDDING_MODEL= LANGSMITH_TRACING=false LANGSMITH_API_KEY= diff --git a/backend-ai/app/clients/embedding_client.py b/backend-ai/app/clients/embedding_client.py new file mode 100644 index 0000000..4f46a89 --- /dev/null +++ b/backend-ai/app/clients/embedding_client.py @@ -0,0 +1,45 @@ +from typing import Any + +import httpx + +from app.core.config import get_settings + + +class EmbeddingClient: + """HTTP boundary for query embeddings.""" + + def __init__( + self, + api_key: str | None = None, + base_url: str | None = None, + model: str | None = None, + timeout_seconds: float = 10.0, + ) -> None: + settings = get_settings() + self.api_key = api_key if api_key is not None else settings.embedding_api_key + self.base_url = (base_url or settings.embedding_base_url).rstrip("/") + self.model = model or settings.embedding_model + self.timeout_seconds = timeout_seconds + + def embed_query(self, query: str) -> list[float]: + if not query.strip(): + raise ValueError("query must not be blank.") + if not self.api_key or not self.model: + raise RuntimeError("Embedding client is not configured.") + + response = httpx.post( + f"{self.base_url}/embeddings", + headers={"Authorization": f"Bearer {self.api_key}"}, + json={"model": self.model, "input": query}, + timeout=self.timeout_seconds, + ) + response.raise_for_status() + payload: dict[str, Any] = response.json() + data = payload.get("data") + if not isinstance(data, list) or not data or not isinstance(data[0], dict): + raise RuntimeError("Embedding response did not include data.") + + embedding = data[0].get("embedding") + if not isinstance(embedding, list): + raise RuntimeError("Embedding response did not include an embedding vector.") + return [float(value) for value in embedding] diff --git a/backend-ai/app/clients/supabase_client.py b/backend-ai/app/clients/supabase_client.py index 30f2d54..610d761 100644 --- a/backend-ai/app/clients/supabase_client.py +++ b/backend-ai/app/clients/supabase_client.py @@ -1,5 +1,8 @@ from typing import Any +import psycopg +from psycopg.rows import dict_row + from app.core.config import get_settings @@ -9,22 +12,31 @@ class SupabaseVectorClient: def __init__(self) -> None: self.database_url = get_settings().supabase_db_url - def similarity_search_legal_documents(self, query: str, top_k: int = 3) -> list[dict[str, Any]]: - return [ - { - "lawName": "주택임대차보호법", - "articleNo": "제3조", - "title": "대항력", - "source": "legal-stub", - "content": "임차인은 주택의 인도와 주민등록을 마친 때에는 그 다음 날부터 제3자에 대하여 효력이 생깁니다.", - "score": 0.91, - }, - { - "lawName": "주택임대차보호법", - "articleNo": "제3조의2", - "title": "보증금의 회수", - "source": "legal-stub", - "content": "확정일자를 갖춘 임차인은 경매 또는 공매 시 후순위권리자보다 우선하여 보증금을 변제받을 수 있습니다.", - "score": 0.86, - }, - ][:top_k] + def similarity_search_legal_documents( + self, + query_embedding: list[float], + top_k: int = 3, + ) -> list[dict[str, Any]]: + vector_literal = to_pgvector_literal(query_embedding) + sql = """ + select + law_name, + article_no, + article_title, + content, + 1 - (embedding <=> %s::vector) as score + from public.legal_document_chunks + where embedding is not null + order by embedding <=> %s::vector + limit %s + """ + with psycopg.connect(self.database_url, row_factory=dict_row) as conn: + with conn.cursor() as cursor: + cursor.execute(sql, (vector_literal, vector_literal, top_k)) + return list(cursor.fetchall()) + + +def to_pgvector_literal(embedding: list[float]) -> str: + if not embedding: + raise ValueError("query_embedding must not be empty.") + return "[" + ",".join(f"{float(value):.10g}" for value in embedding) + "]" diff --git a/backend-ai/app/core/config.py b/backend-ai/app/core/config.py index 1d9fc5a..9b6c0d7 100644 --- a/backend-ai/app/core/config.py +++ b/backend-ai/app/core/config.py @@ -18,6 +18,11 @@ class Settings(BaseSettings): supabase_service_role_key: str = Field(default="", alias="SUPABASE_SERVICE_ROLE_KEY") gms_api_key: str = Field(default="", alias="GMS_API_KEY") llm_model: str = Field(default="", alias="LLM_MODEL") + embedding_api_key: str = Field(default="", alias="EMBEDDING_API_KEY") + embedding_base_url: str = Field( + default="https://api.openai.com/v1", + alias="EMBEDDING_BASE_URL", + ) embedding_model: str = Field(default="", alias="EMBEDDING_MODEL") langsmith_tracing: bool = Field(default=False, alias="LANGSMITH_TRACING") langsmith_api_key: str = Field(default="", alias="LANGSMITH_API_KEY") diff --git a/backend-ai/app/graph/nodes/legal_rag.py b/backend-ai/app/graph/nodes/legal_rag.py index 92c4d3e..6be8199 100644 --- a/backend-ai/app/graph/nodes/legal_rag.py +++ b/backend-ai/app/graph/nodes/legal_rag.py @@ -10,6 +10,6 @@ def legal_rag(state: AgentState) -> AgentState: "legal_cards": cards, "tool_results": { **state.get("tool_results", {}), - "legalRag": {"topK": len(cards), "source": "supabase-pgvector-stub"}, + "legalRag": {"topK": len(cards), "source": "supabase-pgvector"}, }, } diff --git a/backend-ai/app/rag/retriever.py b/backend-ai/app/rag/retriever.py index e08a370..ff5ab52 100644 --- a/backend-ai/app/rag/retriever.py +++ b/backend-ai/app/rag/retriever.py @@ -1,11 +1,53 @@ -from typing import Any +from typing import Any, Protocol +from app.clients.embedding_client import EmbeddingClient from app.clients.supabase_client import SupabaseVectorClient +class EmbeddingClientProtocol(Protocol): + def embed_query(self, query: str) -> list[float]: + ... + + +class VectorClientProtocol(Protocol): + def similarity_search_legal_documents( + self, + query_embedding: list[float], + top_k: int, + ) -> list[dict[str, Any]]: + ... + + class LegalRetriever: - def __init__(self) -> None: - self.vector_client = SupabaseVectorClient() + def __init__( + self, + embedding_client: EmbeddingClientProtocol | None = None, + vector_client: VectorClientProtocol | None = None, + max_top_k: int = 5, + ) -> None: + self.embedding_client = embedding_client or EmbeddingClient() + self.vector_client = vector_client or SupabaseVectorClient() + self.max_top_k = max_top_k def retrieve(self, query: str, top_k: int = 3) -> list[dict[str, Any]]: - return self.vector_client.similarity_search_legal_documents(query=query, top_k=top_k) + normalized_query = query.strip() + if not normalized_query: + return [] + + safe_top_k = min(max(top_k, 1), self.max_top_k) + query_embedding = self.embedding_client.embed_query(normalized_query) + rows = self.vector_client.similarity_search_legal_documents( + query_embedding=query_embedding, + top_k=safe_top_k, + ) + return [normalize_legal_card(row) for row in rows] + + +def normalize_legal_card(row: dict[str, Any]) -> dict[str, Any]: + return { + "lawName": row.get("lawName") or row["law_name"], + "articleNo": row.get("articleNo") or row["article_no"], + "title": row.get("title") or row["article_title"], + "content": row["content"], + "score": float(row["score"]), + } diff --git a/backend-ai/tests/test_agent_chat.py b/backend-ai/tests/test_agent_chat.py index b8aecc1..07c2660 100644 --- a/backend-ai/tests/test_agent_chat.py +++ b/backend-ai/tests/test_agent_chat.py @@ -1,6 +1,8 @@ from fastapi.testclient import TestClient +from app.api.routes import get_agent_graph from app.core.config import get_settings +from app.graph.nodes import legal_rag as legal_rag_module from app.graph.nodes.classify_intent import classify_message from app.graph.state import Intent from app.main import app @@ -46,14 +48,29 @@ def test_agent_chat_returns_intent_and_answer() -> None: assert "properties" in body -def test_agent_chat_returns_legal_cards_for_legal_question() -> None: +def test_agent_chat_returns_legal_cards_for_legal_question(monkeypatch) -> None: + class FakeRetriever: + def retrieve(self, query: str, top_k: int = 3) -> list[dict]: + return [ + { + "lawName": "주택임대차보호법", + "articleNo": "제3조의2", + "title": "보증금의 회수", + "content": "임차인은 보증금을 우선변제받을 권리가 있다.", + "score": 0.86, + } + ] + + monkeypatch.setattr(legal_rag_module, "LegalRetriever", FakeRetriever) + get_agent_graph.cache_clear() + response = client.post( "/internal/agent/chat", headers=internal_api_headers(), json={ "userId": "user-1", "sessionId": None, - "message": "확정일자는 언제 받아야 하나요?", + "message": "계약 전 보증금 반환 관련 법을 알려줘", "context": {"selectedPropertyId": None, "recentMessages": []}, }, ) @@ -73,7 +90,7 @@ def test_agent_chat_returns_legal_cards_for_legal_question() -> None: def test_classify_intent_examples() -> None: assert classify_message("관악구 보증금 5천 이하 원룸 추천해줘") == Intent.PROPERTY_SEARCH - assert classify_message("전세사기 계약이면 어떻게 해야 해?") == Intent.LEGAL_CONSULT - assert classify_message("이 매물 시세가 비싼 편이야?") == Intent.PRICE_ANALYSIS - assert classify_message("주변 치안과 CCTV는 괜찮아?") == Intent.SAFETY_ANALYSIS - assert classify_message("HUG 보증보험 가입 가능해?") == Intent.HUG_CALC + assert classify_message("계약 전에 법을 확인하고 싶어") == Intent.LEGAL_CONSULT + assert classify_message("이 매물 가격이 비싼 편이야?") == Intent.PRICE_ANALYSIS + assert classify_message("주변 cctv는 괜찮아?") == Intent.SAFETY_ANALYSIS + assert classify_message("hug 보증보험 가능해?") == Intent.HUG_CALC diff --git a/backend-ai/tests/test_legal_retriever.py b/backend-ai/tests/test_legal_retriever.py new file mode 100644 index 0000000..8d958be --- /dev/null +++ b/backend-ai/tests/test_legal_retriever.py @@ -0,0 +1,120 @@ +import pytest + +from app.graph.nodes import legal_rag as legal_rag_module +from app.graph.state import Intent +from app.rag.retriever import LegalRetriever + + +class FakeEmbeddingClient: + def __init__(self) -> None: + self.queries: list[str] = [] + + def embed_query(self, query: str) -> list[float]: + self.queries.append(query) + return [0.1, 0.2, 0.3] + + +class FakeVectorClient: + def __init__(self, rows): + self.rows = rows + self.calls: list[dict] = [] + + def similarity_search_legal_documents( + self, + query_embedding: list[float], + top_k: int, + ) -> list[dict]: + self.calls.append({"query_embedding": query_embedding, "top_k": top_k}) + return self.rows[:top_k] + + +def test_legal_retriever_normalizes_pgvector_rows() -> None: + embedding_client = FakeEmbeddingClient() + vector_client = FakeVectorClient( + [ + { + "law_name": "주택임대차보호법", + "article_no": "제3조의2", + "article_title": "보증금의 회수", + "content": "임차인은 보증금을 우선변제받을 권리가 있다.", + "score": 0.91, + } + ] + ) + retriever = LegalRetriever( + embedding_client=embedding_client, + vector_client=vector_client, + ) + + cards = retriever.retrieve("보증금은 어떻게 돌려받나요?", top_k=3) + + assert embedding_client.queries == ["보증금은 어떻게 돌려받나요?"] + assert vector_client.calls == [{"query_embedding": [0.1, 0.2, 0.3], "top_k": 3}] + assert cards == [ + { + "lawName": "주택임대차보호법", + "articleNo": "제3조의2", + "title": "보증금의 회수", + "content": "임차인은 보증금을 우선변제받을 권리가 있다.", + "score": 0.91, + } + ] + + +def test_legal_retriever_returns_empty_for_blank_query_without_external_calls() -> None: + embedding_client = FakeEmbeddingClient() + vector_client = FakeVectorClient([]) + retriever = LegalRetriever( + embedding_client=embedding_client, + vector_client=vector_client, + ) + + assert retriever.retrieve(" ") == [] + assert embedding_client.queries == [] + assert vector_client.calls == [] + + +def test_legal_retriever_clamps_top_k() -> None: + embedding_client = FakeEmbeddingClient() + vector_client = FakeVectorClient([]) + retriever = LegalRetriever( + embedding_client=embedding_client, + vector_client=vector_client, + max_top_k=5, + ) + + retriever.retrieve("대항력", top_k=99) + + assert vector_client.calls[0]["top_k"] == 5 + + +def test_legal_rag_node_records_tool_metadata(monkeypatch: pytest.MonkeyPatch) -> None: + class FakeRetriever: + def retrieve(self, query: str, top_k: int = 3) -> list[dict]: + return [ + { + "lawName": "주택임대차보호법", + "articleNo": "제3조", + "title": "대항력", + "content": "임차인은 대항력을 취득한다.", + "score": 0.88, + } + ] + + monkeypatch.setattr(legal_rag_module, "LegalRetriever", FakeRetriever) + + result = legal_rag_module.legal_rag( + { + "user_id": "user-1", + "session_id": None, + "message": "대항력이 뭐예요?", + "context": {}, + "intent": Intent.LEGAL_CONSULT, + } + ) + + assert len(result["legal_cards"]) == 1 + assert result["tool_results"]["legalRag"] == { + "topK": 1, + "source": "supabase-pgvector", + } diff --git a/phases/ai-legal-rag/phase3-legal-rag-retriever.md b/phases/ai-legal-rag/phase3-legal-rag-retriever.md new file mode 100644 index 0000000..a2f663f --- /dev/null +++ b/phases/ai-legal-rag/phase3-legal-rag-retriever.md @@ -0,0 +1,27 @@ +# Phase 3: Legal RAG Retriever + +## Goal +Replace the legal RAG stub with a testable backend-ai retriever boundary that prepares query embeddings, performs pgvector similarity search, and returns normalized legal cards. This phase keeps the implementation offline-testable by injecting fake embedding and vector clients in tests. + +## Files +- `backend-ai/tests/test_legal_retriever.py` - tests for legal retriever normalization, empty queries, and pgvector client delegation +- `backend-ai/app/rag/retriever.py` - legal RAG retrieval service with dependency injection +- `backend-ai/app/clients/supabase_client.py` - pgvector SQL client boundary for legal document chunks +- `backend-ai/app/clients/embedding_client.py` - embedding client boundary driven by environment configuration +- `backend-ai/app/graph/nodes/legal_rag.py` - legal_rag node using the retriever boundary + +## Done When +- [ ] Legal retriever tests pass without network or database access +- [ ] Stubbed legal cards are removed from production client code +- [ ] Retriever validates blank queries and clamps top_k to a safe range +- [ ] Supabase pgvector search maps rows to legal card fields expected by the chat contract +- [ ] `legal_rag` node records tool metadata without Spring or frontend direct pgvector access + +## Architecture Rules +- pgvector similarity search belongs only in FastAPI `backend-ai`. +- Spring Boot must not call LLMs or pgvector directly. +- API keys and database URLs must be read from environment settings only. +- F-3 MVP legal RAG is limited to official legal document chunks; news RAG and registry AI are out of scope. + +## Implementation Instructions +Write tests first. Keep the retriever injectable so tests can use fake embedding and vector clients. Do not require live Supabase or OpenAI calls in unit tests. Production code may define the SQL and client boundary, but it must not hardcode secrets or fallback to fake legal content. From 904dbb750eaaf74fe8cbd75e28a01c7c38386513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=ED=9C=98?= Date: Mon, 22 Jun 2026 13:40:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(ai):=20=EB=B2=95=EB=A5=A0=20RAG=20DB=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-ai/.env.example | 2 + backend-ai/app/clients/supabase_client.py | 29 ++++++++++++-- backend-ai/app/core/config.py | 8 ++++ backend-ai/tests/test_legal_retriever.py | 47 +++++++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/backend-ai/.env.example b/backend-ai/.env.example index 897cd2e..0c45712 100644 --- a/backend-ai/.env.example +++ b/backend-ai/.env.example @@ -2,6 +2,8 @@ APP_ENV=local INTERNAL_API_KEY=change-me SPRING_API_BASE_URL=http://localhost:8080 SUPABASE_DB_URL=postgresql://user:password@host:5432/postgres +SUPABASE_CONNECT_TIMEOUT_SECONDS=5 +SUPABASE_STATEMENT_TIMEOUT_MS=5000 SUPABASE_SERVICE_ROLE_KEY= GMS_API_KEY= LLM_MODEL= diff --git a/backend-ai/app/clients/supabase_client.py b/backend-ai/app/clients/supabase_client.py index 610d761..f33b3a4 100644 --- a/backend-ai/app/clients/supabase_client.py +++ b/backend-ai/app/clients/supabase_client.py @@ -9,8 +9,23 @@ class SupabaseVectorClient: """Client boundary for Supabase PostgreSQL + pgvector legal search.""" - def __init__(self) -> None: - self.database_url = get_settings().supabase_db_url + def __init__( + self, + connect_timeout_seconds: int | None = None, + statement_timeout_ms: int | None = None, + ) -> None: + settings = get_settings() + self.database_url = settings.supabase_db_url + self.connect_timeout_seconds = ( + connect_timeout_seconds + if connect_timeout_seconds is not None + else settings.supabase_connect_timeout_seconds + ) + self.statement_timeout_ms = ( + statement_timeout_ms + if statement_timeout_ms is not None + else settings.supabase_statement_timeout_ms + ) def similarity_search_legal_documents( self, @@ -30,8 +45,16 @@ def similarity_search_legal_documents( order by embedding <=> %s::vector limit %s """ - with psycopg.connect(self.database_url, row_factory=dict_row) as conn: + with psycopg.connect( + self.database_url, + row_factory=dict_row, + connect_timeout=self.connect_timeout_seconds, + ) as conn: with conn.cursor() as cursor: + cursor.execute( + "set local statement_timeout = %s", + (self.statement_timeout_ms,), + ) cursor.execute(sql, (vector_literal, vector_literal, top_k)) return list(cursor.fetchall()) diff --git a/backend-ai/app/core/config.py b/backend-ai/app/core/config.py index 9b6c0d7..db7344f 100644 --- a/backend-ai/app/core/config.py +++ b/backend-ai/app/core/config.py @@ -15,6 +15,14 @@ class Settings(BaseSettings): default="postgresql://user:password@host:5432/postgres", alias="SUPABASE_DB_URL", ) + supabase_connect_timeout_seconds: int = Field( + default=5, + alias="SUPABASE_CONNECT_TIMEOUT_SECONDS", + ) + supabase_statement_timeout_ms: int = Field( + default=5000, + alias="SUPABASE_STATEMENT_TIMEOUT_MS", + ) supabase_service_role_key: str = Field(default="", alias="SUPABASE_SERVICE_ROLE_KEY") gms_api_key: str = Field(default="", alias="GMS_API_KEY") llm_model: str = Field(default="", alias="LLM_MODEL") diff --git a/backend-ai/tests/test_legal_retriever.py b/backend-ai/tests/test_legal_retriever.py index 8d958be..a94fffe 100644 --- a/backend-ai/tests/test_legal_retriever.py +++ b/backend-ai/tests/test_legal_retriever.py @@ -1,5 +1,7 @@ import pytest +from app.clients import supabase_client as supabase_module +from app.clients.supabase_client import SupabaseVectorClient from app.graph.nodes import legal_rag as legal_rag_module from app.graph.state import Intent from app.rag.retriever import LegalRetriever @@ -118,3 +120,48 @@ def retrieve(self, query: str, top_k: int = 3) -> list[dict]: "topK": 1, "source": "supabase-pgvector", } + + +def test_supabase_vector_client_sets_connection_and_statement_timeouts( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: dict[str, object] = {} + + class FakeCursor: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, traceback) -> None: + return None + + def execute(self, sql, params) -> None: + calls.setdefault("executes", []).append((sql, params)) + + def fetchall(self) -> list[dict]: + return [] + + class FakeConnection: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, traceback) -> None: + return None + + def cursor(self) -> FakeCursor: + return FakeCursor() + + def fake_connect(database_url, **kwargs): + calls["database_url"] = database_url + calls["connect_kwargs"] = kwargs + return FakeConnection() + + monkeypatch.setattr(supabase_module.psycopg, "connect", fake_connect) + client = SupabaseVectorClient( + connect_timeout_seconds=7, + statement_timeout_ms=3000, + ) + + client.similarity_search_legal_documents([0.1, 0.2], top_k=2) + + assert calls["connect_kwargs"]["connect_timeout"] == 7 + assert calls["executes"][0] == ("set local statement_timeout = %s", (3000,)) From c3c3ab5f3b3cd512971405d2c6c41fca169d163b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=ED=9C=98?= Date: Mon, 22 Jun 2026 13:47:02 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test(ai):=20=EB=B2=95=EB=A5=A0=20RAG=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B3=B4=EC=99=84=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-ai/.env.example | 2 +- backend-ai/tests/test_legal_retriever.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend-ai/.env.example b/backend-ai/.env.example index 0c45712..c0d0ba8 100644 --- a/backend-ai/.env.example +++ b/backend-ai/.env.example @@ -1,8 +1,8 @@ APP_ENV=local INTERNAL_API_KEY=change-me SPRING_API_BASE_URL=http://localhost:8080 -SUPABASE_DB_URL=postgresql://user:password@host:5432/postgres SUPABASE_CONNECT_TIMEOUT_SECONDS=5 +SUPABASE_DB_URL=postgresql://user:password@host:5432/postgres SUPABASE_STATEMENT_TIMEOUT_MS=5000 SUPABASE_SERVICE_ROLE_KEY= GMS_API_KEY= diff --git a/backend-ai/tests/test_legal_retriever.py b/backend-ai/tests/test_legal_retriever.py index a94fffe..fe71b70 100644 --- a/backend-ai/tests/test_legal_retriever.py +++ b/backend-ai/tests/test_legal_retriever.py @@ -164,4 +164,5 @@ def fake_connect(database_url, **kwargs): client.similarity_search_legal_documents([0.1, 0.2], top_k=2) assert calls["connect_kwargs"]["connect_timeout"] == 7 + assert len(calls.get("executes", [])) >= 1 assert calls["executes"][0] == ("set local statement_timeout = %s", (3000,))