refactor(memory): extract F2 recall path (#23)

This commit is contained in:
2026-08-24 01:08:15 +02:00
parent beac2e80f4
commit 93fe0d733b
8 changed files with 585 additions and 236 deletions
+168
View File
@@ -0,0 +1,168 @@
import json
from datetime import UTC, datetime
from types import SimpleNamespace
import uuid
from typer.testing import CliRunner
from tht.cli import app
from tht.decisions import DecisionInput
from tht.memory import MemoryRecord, recall_memories, save_registry
from tht.phase import current_phase
from tht.session.filesystem_repository import FilesystemSessionRepository
from tht.session.models import PrincipalContext, SessionManifest
def _memory(id_: str, type_: str = "concept_clarified") -> MemoryRecord:
return MemoryRecord(
id=id_,
ts=datetime(2026, 8, 24, tzinfo=UTC),
session_id="source-session",
decision_seq=int(id_.split("-")[1]),
type=type_,
subject=f"subject {id_}",
detail=f"detail {id_}",
rationale=f"rationale {id_}",
question_context="source question",
concepts=[f"concept {id_}"],
)
class Embedder:
def __init__(self):
self.questions = []
def embed_query(self, question):
self.questions.append(question)
return [0.1, 0.2]
class Searcher:
def __init__(self, hits):
self.hits = hits
self.calls = []
def search(self, embedding, *, top_n, kinds):
self.calls.append((embedding, top_n, kinds))
return self.hits
def test_recall_preserves_rank_and_public_payload_for_reusable_memories():
records = [_memory("mem-0001"), _memory("mem-0002", "table_promoted"), _memory("mem-0003")]
searcher = Searcher([
SimpleNamespace(ref="mem-0003", similarity=0.93456),
SimpleNamespace(ref="mem-0002", similarity=0.92345),
SimpleNamespace(ref="orphan", similarity=0.91234),
SimpleNamespace(ref="mem-0001", similarity=0.87654),
])
embedder = Embedder()
results = recall_memories(
"active patients",
records=records,
decisions=[],
searcher=searcher,
embedder=embedder,
top=5,
)
assert [result["id"] for result in results] == ["mem-0003", "mem-0001"]
assert results[0] == {
"id": "mem-0003",
"type": "concept_clarified",
"subject": "subject mem-0003",
"detail": "detail mem-0003",
"rationale": "rationale mem-0003",
"question_context": "source question",
"tables": [],
"concepts": ["concept mem-0003"],
"session_id": "source-session",
"score": 0.9346,
}
assert embedder.questions == ["active patients"]
assert searcher.calls == [([0.1, 0.2], 5, ["memory"])]
def _workspace_config(tmp_path):
config = tmp_path / "workspace.yaml"
config.write_text(
f"""
runtime_identity:
workspace_id: psd-clinical
workspace_revision: {'a' * 40}
dwh:
type: postgres_direct
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
vectors:
type: qdrant
base_url: http://qdrant:6333
collection: psd-clinical
roots:
sessions: {tmp_path / 'sessions'}
artifacts: {tmp_path / 'artifacts'}
indexes: {tmp_path / 'indexes'}
embeddings:
provider: ollama_internal
base_url: http://embedding:11434
model: qwen3-embedding:0.6b
dim: 1024
"""
)
return config
def test_recall_cli_reconstructs_applied_and_rejected_memory_from_persisted_f2_session(
tmp_path, monkeypatch
):
monkeypatch.setenv("THT_HOME", str(tmp_path / "home"))
repository = FilesystemSessionRepository(
tmp_path / "home",
"psd-clinical",
PrincipalContext(issuer="local", subject="reviewer"),
root=tmp_path / "sessions",
)
session_id = str(uuid.uuid4())
repository.create(SessionManifest(
id=session_id,
created_at=datetime(2026, 8, 24, tzinfo=UTC),
question="active patients",
database="analytics",
schema="mart",
))
repository.append_decisions(session_id, [
DecisionInput(type="phase_approved", subject="phase:1"),
DecisionInput(
type="concept_clarified",
subject="active patient",
rationale="Applied from mem-0003",
),
DecisionInput(
type="memory_rejected",
subject="mem-0001",
rationale="Not relevant to the resumed question",
),
])
records = [_memory("mem-0001"), _memory("mem-0003")]
searcher = Searcher([
SimpleNamespace(ref="mem-0003", similarity=0.9),
SimpleNamespace(ref="mem-0001", similarity=0.8),
])
embedder = Embedder()
save_registry(records, tmp_path / "artifacts" / "memory" / "registry.jsonl")
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda cfg: searcher)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda cfg: embedder)
response = CliRunner().invoke(
app,
[
"memory", "search", "active patients", "--session", session_id,
"--json", "-c", str(_workspace_config(tmp_path)),
],
)
assert response.exit_code == 0, response.output
assert json.loads(response.stdout) == []
assert current_phase(repository.get(session_id)) == 2
assert embedder.questions == ["active patients"]
assert searcher.calls == [([0.1, 0.2], 5, ["memory"])]