feat(harness): arricchisci metadata memory (subject/detail/rationale) — Onda 3.1 TDD
Correzione del gap ereditato (resosi NECESSARIO dal drop del registry, spec 5): il metadata del VectorRecord memory ora porta subject/detail/rationale oltre a type/session_id/tables/concepts. pack_metadata li serializza nel jsonb via **record.metadata. search_similar proietta metadata completo -> la F2 ricostruisce la decisione direttamente dall'hit, senza lookup registro. L1: 4 test (subject/detail/rationale presenti, campi esistenti preservati, no cross-contamination multi-record, save_one_memory propaga il metadata alla riga). Suite: 165 passed.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""L1: arricchimento metadata memory — subject/detail/rationale nel jsonb (decisione spec 6).
|
||||
|
||||
Una volta che il registry e' droppato (decisione spec 5), il vectordb e' l'unica
|
||||
fonte delle memory. L'hit di search_similar deve bastare per ricostruire la decisione
|
||||
completa. Per questo memory_vector_records mette subject/detail/rationale nel metadata
|
||||
del VectorRecord (pack_metadata li serializza nel jsonb via **record.metadata).
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from tht.memory import MemoryRecord, memory_vector_records
|
||||
|
||||
|
||||
def _record(**kw) -> MemoryRecord:
|
||||
base = dict(
|
||||
id="mem-x", ts=datetime(2025, 1, 1), session_id="s", decision_seq=1,
|
||||
type="table_promoted", subject="dim_pazienti", detail="promossa",
|
||||
rationale="perche' serve", question_context="dammi pazienti",
|
||||
tables=["t"], concepts=[],
|
||||
)
|
||||
base.update(kw)
|
||||
return MemoryRecord(**base)
|
||||
|
||||
|
||||
def test_memory_vector_record_has_subject_detail_rationale_in_metadata():
|
||||
vr = memory_vector_records([_record()])[0]
|
||||
assert vr.metadata["subject"] == "dim_pazienti"
|
||||
assert vr.metadata["detail"] == "promossa"
|
||||
assert vr.metadata["rationale"] == "perche' serve"
|
||||
|
||||
|
||||
def test_memory_vector_record_metadata_keeps_existing_fields():
|
||||
vr = memory_vector_records([_record()])[0]
|
||||
# i campi che gia' c'erano restano (backward compat)
|
||||
assert vr.metadata["type"] == "table_promoted"
|
||||
assert vr.metadata["tables"] == ["t"]
|
||||
assert vr.metadata["concepts"] == []
|
||||
assert vr.metadata["session_id"] == "s"
|
||||
|
||||
|
||||
def test_each_record_carries_its_own_subject():
|
||||
# piu' record, ciascuno con il proprio subject (no cross-contamination)
|
||||
recs = [
|
||||
_record(id="m1", subject="t_a", detail="d_a", rationale="r_a"),
|
||||
_record(id="m2", subject="t_b", detail="d_b", rationale="r_b"),
|
||||
]
|
||||
out = memory_vector_records(recs)
|
||||
assert {v.metadata["subject"] for v in out} == {"t_a", "t_b"}
|
||||
assert {v.metadata["detail"] for v in out} == {"d_a", "d_b"}
|
||||
|
||||
|
||||
def test_save_one_memory_preserves_subject_through_upsert_row():
|
||||
# end-to-end: save_one_memory costruisce la riga upsert; il metadata deve arrivare
|
||||
# con subject/detail/rationale (pack_metadata lo prende da record.metadata).
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from tht.memory import save_one_memory
|
||||
|
||||
writer = MagicMock()
|
||||
writer.upsert_records.return_value = 1
|
||||
embedder = MagicMock()
|
||||
embedder.embed_documents.return_value = [[0.0] * 8]
|
||||
save_one_memory([_record(decision_seq=1)], decision_seq=1, writer=writer, embedder=embedder)
|
||||
row = writer.upsert_records.call_args[0][1][0]
|
||||
md = row["metadata"]
|
||||
assert md["subject"] == "dim_pazienti"
|
||||
assert md["detail"] == "promossa"
|
||||
assert md["rationale"] == "perche' serve"
|
||||
@@ -207,6 +207,7 @@ def memory_vector_records(records: list[MemoryRecord]) -> list[VectorRecord]:
|
||||
metadata={
|
||||
"type": r.type, "session_id": r.session_id,
|
||||
"tables": r.tables, "concepts": r.concepts,
|
||||
"subject": r.subject, "detail": r.detail, "rationale": r.rationale,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user