169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
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"])]
|