fix: harden reviewer workflow and memory handling
This commit is contained in:
@@ -34,9 +34,10 @@ def test_save_one_upserts_to_real_pgvector(l2_env):
|
||||
|
||||
record = MemoryRecord(
|
||||
id="mem-l2test", ts=datetime.now(), session_id="l2-self-test",
|
||||
decision_seq=999, type="table_promoted", subject="fct_ricoveri",
|
||||
detail="ablazione", rationale="L2 self-test (idempotent)",
|
||||
question_context="ablazione 2025", tables=["fct_ricoveri"], concepts=[],
|
||||
decision_seq=999, type="concept_clarified", subject="ablazione recente",
|
||||
detail="evento di ablazione negli ultimi 15 anni",
|
||||
rationale="L2 self-test (idempotent)",
|
||||
question_context="ablazione 2025", tables=[], concepts=["ablazione recente"],
|
||||
)
|
||||
from tht.adapters.vector import ThothHttpVectorStore
|
||||
|
||||
|
||||
@@ -68,9 +68,11 @@ def test_memory_command_writes_through_factory_vector_store(monkeypatch):
|
||||
cfg = SimpleNamespace(profile="server", embeddings=object(), vector_write_rest=None)
|
||||
manifest = SimpleNamespace(id="s1")
|
||||
snapshot = SimpleNamespace(manifest=manifest, decisions=[], artifacts={})
|
||||
record = MemoryRecord(id="m1", ts=datetime(2026, 1, 1), session_id="s1",
|
||||
decision_seq=7, type="table_promoted", subject="t",
|
||||
question_context="q")
|
||||
record = MemoryRecord(
|
||||
id="m1", ts=datetime(2026, 1, 1), session_id="s1",
|
||||
decision_seq=7, type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE", question_context="q",
|
||||
)
|
||||
monkeypatch.setattr(memory_cmd, "_load_config_or_exit", lambda path: cfg)
|
||||
monkeypatch.setattr(memory_cmd, "load_snapshot_or_exit", lambda cfg, session: snapshot)
|
||||
monkeypatch.setattr(memory_cmd, "registry_path", lambda cfg: None)
|
||||
|
||||
@@ -16,7 +16,7 @@ from tht.workflow import load_workflow
|
||||
("concept_clarified", 1),
|
||||
("memory_rejected", 2),
|
||||
("question_rewritten", 3),
|
||||
("table_promoted", 2),
|
||||
("table_promoted", 4),
|
||||
("column_corrected", 4),
|
||||
("evidence_accepted", 4),
|
||||
("value_grounded", 4),
|
||||
|
||||
@@ -13,9 +13,9 @@ 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",
|
||||
type="concept_clarified", subject="paziente attivo", detail="flag_attivo = TRUE",
|
||||
rationale="perche' serve", question_context="dammi pazienti",
|
||||
tables=["t"], concepts=[],
|
||||
tables=[], concepts=["paziente attivo"],
|
||||
)
|
||||
base.update(kw)
|
||||
return MemoryRecord(**base)
|
||||
@@ -23,17 +23,17 @@ def _record(**kw) -> MemoryRecord:
|
||||
|
||||
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["subject"] == "paziente attivo"
|
||||
assert vr.metadata["detail"] == "flag_attivo = TRUE"
|
||||
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["type"] == "concept_clarified"
|
||||
assert vr.metadata["tables"] == []
|
||||
assert vr.metadata["concepts"] == ["paziente attivo"]
|
||||
assert vr.metadata["session_id"] == "s"
|
||||
|
||||
|
||||
@@ -62,6 +62,6 @@ def test_save_one_memory_preserves_subject_through_upsert_row():
|
||||
save_one_memory([_record(decision_seq=1)], decision_seq=1, store=writer, embedder=embedder)
|
||||
row = writer.upsert.call_args[0][1][0]
|
||||
md = row.record.metadata
|
||||
assert md["subject"] == "dim_pazienti"
|
||||
assert md["detail"] == "promossa"
|
||||
assert md["subject"] == "paziente attivo"
|
||||
assert md["detail"] == "flag_attivo = TRUE"
|
||||
assert md["rationale"] == "perche' serve"
|
||||
|
||||
@@ -9,7 +9,13 @@ la rende un passo del workflow. Questi test fissano il contratto harness-side:
|
||||
from datetime import datetime
|
||||
|
||||
from tht.decisions import DecisionRecord, append_decision
|
||||
from tht.memory import declined_promotion_seqs, reusable_promotions
|
||||
from tht.memory import (
|
||||
MemoryRecord,
|
||||
declined_promotion_seqs,
|
||||
memory_vector_records,
|
||||
promote,
|
||||
reusable_promotions,
|
||||
)
|
||||
from tht.session.models import SessionManifest
|
||||
from tht.workflow import load_workflow
|
||||
|
||||
@@ -62,3 +68,48 @@ def test_reusable_promotions_exclude_declined(tmp_path):
|
||||
subject="fact_a", detail="seq:1") # seq 3
|
||||
cand = reusable_promotions(tmp_path, _manifest(), tmp_path / "registry.jsonl")
|
||||
assert [c.decision_seq for c in cand] == [2]
|
||||
|
||||
|
||||
def test_reusable_promotions_deduplicate_identical_memory_content(tmp_path):
|
||||
append_decision(tmp_path, type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE", rationale="scelta reviewer")
|
||||
append_decision(tmp_path, type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE", rationale="scelta reviewer")
|
||||
|
||||
cand = reusable_promotions(tmp_path, _manifest(), tmp_path / "registry.jsonl")
|
||||
|
||||
assert [c.decision_seq for c in cand] == [1]
|
||||
|
||||
|
||||
def test_only_concept_clarified_is_proposed_or_promoted(tmp_path):
|
||||
append_decision(tmp_path, type="table_promoted", subject="fact_a",
|
||||
detail="tabella principale")
|
||||
append_decision(tmp_path, type="table_excluded", subject="fact_b",
|
||||
detail="tabella non pertinente")
|
||||
append_decision(tmp_path, type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE")
|
||||
registry = tmp_path / "registry.jsonl"
|
||||
|
||||
candidates = reusable_promotions(tmp_path, _manifest(), registry)
|
||||
promoted = promote(tmp_path, _manifest(), seqs=[1, 2, 3], registry_path=registry)
|
||||
|
||||
assert [(c.decision_seq, c.type) for c in candidates] == [(3, "concept_clarified")]
|
||||
assert [(c.decision_seq, c.type) for c in promoted] == [(3, "concept_clarified")]
|
||||
|
||||
|
||||
def test_legacy_table_records_are_not_published_as_memory_vectors():
|
||||
records = [
|
||||
MemoryRecord(
|
||||
id="mem-0001", ts=datetime(2026, 1, 1), session_id="s1",
|
||||
decision_seq=1, type="table_promoted", subject="fact_a",
|
||||
),
|
||||
MemoryRecord(
|
||||
id="mem-0002", ts=datetime(2026, 1, 1), session_id="s1",
|
||||
decision_seq=2, type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE",
|
||||
),
|
||||
]
|
||||
|
||||
vectors = memory_vector_records(records)
|
||||
|
||||
assert [record.ref for record in vectors] == ["mem-0002"]
|
||||
|
||||
@@ -16,8 +16,9 @@ from tht.memory import MemoryRecord, memory_vector_record_for_decision, save_one
|
||||
def _record(seq: int = 7, **kw) -> MemoryRecord:
|
||||
base = dict(
|
||||
id="mem-0007", ts=datetime(2025, 1, 1), session_id="s1", decision_seq=seq,
|
||||
type="table_promoted", subject="pazienti", detail="promossa", rationale="r",
|
||||
question_context="dammi i pazienti", tables=["pazienti"], concepts=[],
|
||||
type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE", rationale="r",
|
||||
question_context="dammi i pazienti", tables=[], concepts=["paziente attivo"],
|
||||
)
|
||||
base.update(kw)
|
||||
return MemoryRecord(**base)
|
||||
|
||||
@@ -54,6 +54,10 @@ def test_psd_overlay_uses_generated_workspace_for_default_and_named_commands():
|
||||
assert core["networks"]["default"]["aliases"] == ["core", "thothii-core"]
|
||||
frontend = compose["services"]["frontend"]
|
||||
assert frontend["ports"] == ["127.0.0.1:8099:8080"]
|
||||
assert frontend["build"]["args"] == {
|
||||
"VITE_BASE": "/",
|
||||
"VITE_BACKEND_URL": "/api",
|
||||
}
|
||||
assert frontend["networks"] == {
|
||||
"default": {"aliases": ["frontend", "thothii-frontend"]}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ puro (`[]` in modalita' --json) ed exit 0, cosi' il modello prosegue senza
|
||||
exemplar. Il finalize-hook gestisce gia' lo stesso scenario in modo analogo.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli import app
|
||||
from tht.memory import MemoryRecord, save_registry
|
||||
from tht.ports.vector import VectorReadUnavailable
|
||||
from tht.vectorstore.rest_client import VectorRestError
|
||||
from tht.vectorstore.store import VectorHit
|
||||
@@ -97,3 +99,44 @@ def test_solved_search_json_maps_hit_metadata(tmp_path, monkeypatch):
|
||||
"session_id": "s1", "question": "quante ablazioni nel 2023",
|
||||
"sql": "SELECT 1", "tables": ["fact_seeablazione"], "score": 0.91,
|
||||
}]
|
||||
|
||||
|
||||
def test_memory_search_excludes_legacy_table_records(tmp_path, monkeypatch):
|
||||
records = [
|
||||
MemoryRecord(
|
||||
id="mem-0001", ts=datetime(2026, 1, 1), session_id="s1",
|
||||
decision_seq=1, type="table_promoted", subject="fact_pazienti",
|
||||
),
|
||||
MemoryRecord(
|
||||
id="mem-0002", ts=datetime(2026, 1, 1), session_id="s1",
|
||||
decision_seq=2, type="concept_clarified", subject="paziente attivo",
|
||||
detail="flag_attivo = TRUE",
|
||||
),
|
||||
]
|
||||
cfg = _cfg(tmp_path)
|
||||
save_registry(records, tmp_path / "a" / "memory" / "registry.jsonl")
|
||||
|
||||
class FakeSearcher:
|
||||
def search(self, vec, top_n=10, kinds=None):
|
||||
return [
|
||||
VectorHit(
|
||||
id=f"memory:{record.id}", kind="memory", ref=record.id,
|
||||
title=record.subject, content=record.detail, metadata={},
|
||||
similarity=0.9,
|
||||
)
|
||||
for record in records
|
||||
]
|
||||
|
||||
class FakeEmbedder:
|
||||
def embed_query(self, text):
|
||||
return [0.1] * 8
|
||||
|
||||
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda workspace: FakeSearcher())
|
||||
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda embeddings: FakeEmbedder())
|
||||
|
||||
res = CliRunner().invoke(
|
||||
app, ["memory", "search", "pazienti", "--json", "-c", str(cfg)]
|
||||
)
|
||||
|
||||
assert res.exit_code == 0, res.output
|
||||
assert [record["id"] for record in json.loads(res.stdout)] == ["mem-0002"]
|
||||
|
||||
Reference in New Issue
Block a user