From 159207a8f13473df5d3e215e6b6132edcc6a3c09 Mon Sep 17 00:00:00 2001 From: mptyl Date: Sat, 27 Jun 2026 14:10:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(harness):=20arricchisci=20metadata=20memor?= =?UTF-8?q?y=20(subject/detail/rationale)=20=E2=80=94=20Onda=203.1=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- harness/tests/test_memory_metadata.py | 67 +++++++++++++++++++++++++++ harness/tht/memory.py | 1 + 2 files changed, 68 insertions(+) create mode 100644 harness/tests/test_memory_metadata.py diff --git a/harness/tests/test_memory_metadata.py b/harness/tests/test_memory_metadata.py new file mode 100644 index 00000000..673bb488 --- /dev/null +++ b/harness/tests/test_memory_metadata.py @@ -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" diff --git a/harness/tht/memory.py b/harness/tht/memory.py index e9d5a8b7..cc213636 100644 --- a/harness/tht/memory.py +++ b/harness/tht/memory.py @@ -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, }, ) )