124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
"""L1: filtro `kinds` server-side su search_similar (fast-follow post active-memory).
|
|
|
|
`memory` e `solved_question` condividono la tabella pgvector: senza filtro nel
|
|
`WHERE` della RPC, il top-k della tabella mista puo' affamare la ricerca memorie
|
|
(e viceversa) perche' il filtro per kind avveniva solo client-side DOPO il taglio
|
|
a top_n. Questi test fissano il contratto client:
|
|
- il client manda `kinds` nel payload della RPC quando richiesto (filtro esatto);
|
|
- su un server legacy (funzione a 3 argomenti -> PostgREST 404) ritenta senza
|
|
`kinds`, lasciando il filtro al post-filter client-side esistente;
|
|
- RestSearcher inoltra i kinds alla RPC.
|
|
"""
|
|
import pytest
|
|
|
|
from tht.config import RestConfig
|
|
from tht.vectorstore.reader import RestSearcher
|
|
from tht.vectorstore.rest_client import VectorRestClient, VectorRestError
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, status_code=200, payload=None, text=""):
|
|
self.status_code = status_code
|
|
self._payload = [] if payload is None else payload
|
|
self.text = text or ("[]" if status_code == 200 else text)
|
|
|
|
@property
|
|
def ok(self):
|
|
return self.status_code < 400
|
|
|
|
def json(self):
|
|
if not self.ok:
|
|
return {"message": self.text}
|
|
return self._payload
|
|
|
|
|
|
def _client() -> VectorRestClient:
|
|
return VectorRestClient(RestConfig(base_url="https://v/", api_key="K-READ"))
|
|
|
|
|
|
def test_search_similar_sends_kinds_in_rpc_payload(monkeypatch):
|
|
seen = []
|
|
|
|
def fake_post(url, json=None, **kw):
|
|
seen.append(json)
|
|
return _Resp(payload=[{"similarity": 0.9, "metadata": {"kind": "memory"}}])
|
|
|
|
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
|
|
rows = _client().search_similar("memory", [0.1] * 4, 5, kinds=["memory"])
|
|
assert len(rows) == 1
|
|
assert seen[0]["kinds"] == ["memory"]
|
|
assert seen[0]["table_name"] == "memory"
|
|
assert seen[0]["limit_count"] == 5
|
|
|
|
|
|
def test_search_similar_omits_kinds_when_none(monkeypatch):
|
|
seen = []
|
|
|
|
def fake_post(url, json=None, **kw):
|
|
seen.append(json)
|
|
return _Resp()
|
|
|
|
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
|
|
_client().search_similar("memory", [0.1] * 4, 5)
|
|
assert "kinds" not in seen[0]
|
|
|
|
|
|
def test_search_similar_falls_back_without_kinds_on_legacy_404(monkeypatch):
|
|
# Server legacy: la funzione a 4 argomenti non esiste -> PostgREST 404 (PGRST202).
|
|
# Il client ritenta senza `kinds`; il filtro resta al post-filter client-side.
|
|
seen = []
|
|
|
|
def fake_post(url, json=None, **kw):
|
|
seen.append(json)
|
|
if "kinds" in json:
|
|
return _Resp(status_code=404, text="Could not find the function (PGRST202)")
|
|
return _Resp(payload=[{"similarity": 0.8, "metadata": {"kind": "memory"}}])
|
|
|
|
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
|
|
rows = _client().search_similar("memory", [0.1] * 4, 5, kinds=["memory"])
|
|
assert len(rows) == 1
|
|
assert len(seen) == 2
|
|
assert "kinds" in seen[0] and "kinds" not in seen[1]
|
|
|
|
|
|
def test_search_similar_reraises_non_404_with_kinds(monkeypatch):
|
|
def fake_post(url, json=None, **kw):
|
|
return _Resp(status_code=500, text="boom")
|
|
|
|
monkeypatch.setattr("tht.vectorstore.rest_client.requests.post", fake_post)
|
|
with pytest.raises(VectorRestError, match="HTTP 500"):
|
|
_client().search_similar("memory", [0.1] * 4, 5, kinds=["memory"])
|
|
|
|
|
|
def test_generation_filter_is_sent_exactly_and_legacy_404_fails_closed(monkeypatch):
|
|
calls = []
|
|
|
|
def fake_call(self, function, payload):
|
|
calls.append(payload)
|
|
raise VectorRestError("HTTP 404 missing filtered RPC")
|
|
|
|
monkeypatch.setattr(VectorRestClient, "_call", fake_call)
|
|
metadata_filter = {"vector_generation": "gen:abc", "document_ids": ["doc:1"]}
|
|
with pytest.raises(VectorRestError, match="404"):
|
|
_client().search_similar(
|
|
"evidence", [0.1] * 4, 5, kinds=["evidence"], metadata_filter=metadata_filter
|
|
)
|
|
assert calls == [{
|
|
"query_embedding": [0.1] * 4, "limit_count": 5, "table_name": "evidence",
|
|
"kinds": ["evidence"], "metadata_filter": metadata_filter,
|
|
}]
|
|
|
|
|
|
def test_rest_searcher_forwards_kinds_to_client():
|
|
calls = []
|
|
|
|
class FakeClient:
|
|
def search_similar(self, table_name, query_vec, top_n, kinds=None):
|
|
calls.append((table_name, top_n, kinds))
|
|
return [{"similarity": 0.7, "metadata": {"kind": "solved_question",
|
|
"record_key": "solved:s1"}}]
|
|
|
|
hits = RestSearcher(FakeClient()).search([0.1] * 4, top_n=3, kinds=["solved_question"])
|
|
assert calls == [("memory", 3, ["solved_question"])]
|
|
assert [h.kind for h in hits] == ["solved_question"]
|