feat(reparenting): supersede di un record ri-parenta automaticamente i figli attivi
- gateway: nel blocco supersede, i figli attivi (parent_id == vecchio UUID, superseded_by vuoto) vengono ri-parentati al nuovo UUID; audit action 'reparent'; risposta con campo 'reparented'. Un solo livello: i nipoti puntano agli UUID dei figli, invariati. I figli superseduti restano storici ancorati alla vecchia lineage. - estensione: qmem_tree con fallback lineage (cerca figli anche per supersedes_id del root) per gli orfani pre-fix; qmem_correct riporta il numero di figli ri-parentati. - test: 2 nuovi (ri-parenta figli attivi; ri-parenta solo attivi con figlio già superseduto). 34 pass. - deploy su brain (10.8.0.3): main.py aggiornato, container ricreato, smoke test end-to-end OK (reparented=1).
This commit is contained in:
@@ -70,8 +70,24 @@ class FakeQdrant:
|
||||
for pid in points_selector:
|
||||
self.points.pop(pid, None)
|
||||
|
||||
def scroll(self, collection_name, **kw):
|
||||
return list(self.points.values()), None
|
||||
def _matches(self, pl, query_filter):
|
||||
"""Applica i filtri metadata (FieldCondition match / IsEmptyCondition)."""
|
||||
if not query_filter or not query_filter.must:
|
||||
return True
|
||||
for cond in query_filter.must:
|
||||
if hasattr(cond, "key") and hasattr(cond, "match"):
|
||||
if pl.get(cond.key) != cond.match.value:
|
||||
return False
|
||||
elif hasattr(cond, "is_empty"):
|
||||
if pl.get(cond.is_empty.key):
|
||||
return False
|
||||
return True
|
||||
|
||||
def scroll(self, collection_name, scroll_filter=None, limit=None, with_payload=True, **kw):
|
||||
points = [p for p in self.points.values() if self._matches(p.payload, scroll_filter)]
|
||||
if limit:
|
||||
points = points[:limit]
|
||||
return points, None
|
||||
|
||||
def get_collection(self, collection_name):
|
||||
class _Info:
|
||||
|
||||
@@ -143,6 +143,91 @@ def test_supersede_doppio_409(client):
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
def test_supersede_root_ri_parenta_figli_attivi(client):
|
||||
# L1 root + figlio L2
|
||||
r1 = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(text="root L1", level="L1_ROOT", topic="TEST-TOPIC/ROOT"),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
root_id = r1.json()["memory_id"]
|
||||
r2 = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(text="figlio L2", level="L2_SUBTOPIC", topic="TEST-TOPIC/SUB", parent_id=root_id),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
child_id = r2.json()["memory_id"]
|
||||
|
||||
# supersede il root
|
||||
r3 = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(
|
||||
text="root L1 corretto",
|
||||
level="L1_ROOT",
|
||||
topic="TEST-TOPIC/ROOT",
|
||||
supersedes_id=root_id,
|
||||
supersede_reason="aggiornamento",
|
||||
),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
assert r3.status_code == 200
|
||||
new_root_id = r3.json()["memory_id"]
|
||||
assert r3.json()["reparented"] == 1
|
||||
|
||||
# il figlio attivo ora punta al nuovo root
|
||||
child = client.get(f"/v1/memories/{child_id}", headers=auth_headers()).json()
|
||||
assert child["parent_id"] == new_root_id
|
||||
|
||||
# search per parent_id sul nuovo root trova il figlio
|
||||
s = client.post(
|
||||
"/v1/memories:search",
|
||||
json={"query": "*", "parent_id": new_root_id, "top_k": 10, "min_score": 0.0},
|
||||
headers=auth_headers(),
|
||||
)
|
||||
ids = [x["memory_id"] for x in s.json()["results"]]
|
||||
assert child_id in ids
|
||||
|
||||
|
||||
def test_supersede_root_ri_parenta_solo_figli_attivi(client):
|
||||
# L1 root + figlio L2 + figlio L2 già superseduto (versione attiva C1')
|
||||
r1 = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(text="root", level="L1_ROOT", topic="T2/ROOT"),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
root_id = r1.json()["memory_id"]
|
||||
c1 = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(text="figlio vecchio", level="L2_SUBTOPIC", topic="T2/SUB", parent_id=root_id),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
c1_id = c1.json()["memory_id"]
|
||||
c1p = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(
|
||||
text="figlio nuovo",
|
||||
level="L2_SUBTOPIC",
|
||||
topic="T2/SUB",
|
||||
parent_id=root_id,
|
||||
supersedes_id=c1_id,
|
||||
),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
c1p_id = c1p.json()["memory_id"]
|
||||
|
||||
r2 = client.post(
|
||||
"/v1/memories",
|
||||
json=make_record(text="root corretto", level="L1_ROOT", topic="T2/ROOT", supersedes_id=root_id),
|
||||
headers=auth_headers(),
|
||||
)
|
||||
new_root_id = r2.json()["memory_id"]
|
||||
assert r2.json()["reparented"] == 1 # solo C1' (attivo)
|
||||
|
||||
# C1' ri-parentato al nuovo root; C1 storico resta ancorato al vecchio
|
||||
assert client.get(f"/v1/memories/{c1p_id}", headers=auth_headers()).json()["parent_id"] == new_root_id
|
||||
assert client.get(f"/v1/memories/{c1_id}", headers=auth_headers()).json()["parent_id"] == root_id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ricerca e filtri
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user