tandem / src /llm /cache.py
heisienberg's picture
Initial TANDEM submission: Two-agent LLM recommender with Nigerian persona overlay
1b4d8db verified
Raw
History Blame Contribute Delete
2.9 kB
"""JSONL response cache for LLM calls.
Per research/experiment_matrix.md §0: cache key = SHA-256 of (prompt, model, seed,
temperature). Cache is shipped with the submission so reviewers reproduce numbers
without API access.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Iterator
@dataclass(frozen=True)
class CacheKey:
"""The canonical cache key. Hash determinism matters for reproducibility."""
prompt: str
model: str
seed: int
temperature: float
def to_hash(self) -> str:
payload = json.dumps(asdict(self), sort_keys=True).encode("utf-8")
return hashlib.sha256(payload).hexdigest()
@dataclass
class CacheEntry:
"""A single cached LLM call. Shape mirrors the schema in experiment_matrix.md §2."""
key_hash: str
prompt: str
model: str
seed: int
temperature: float
response_text: str
input_tokens: int
output_tokens: int
latency_ms: int
timestamp: str
def to_jsonl(self) -> str:
return json.dumps(asdict(self), ensure_ascii=False) + "\n"
@classmethod
def from_jsonl(cls, line: str) -> "CacheEntry":
return cls(**json.loads(line))
class JsonlCache:
"""Append-only JSONL cache. Reads load entire file into a hash-indexed dict.
For a hackathon-scale cache (~30k entries, ~30MB), in-memory indexing is fine.
"""
def __init__(self, path: Path | str):
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self._index: dict[str, CacheEntry] = {}
if self.path.exists():
self._load()
def _load(self) -> None:
with self.path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
entry = CacheEntry.from_jsonl(line)
self._index[entry.key_hash] = entry
def get(self, key: CacheKey) -> CacheEntry | None:
return self._index.get(key.to_hash())
def put(self, key: CacheKey, entry: CacheEntry) -> None:
self._index[key.to_hash()] = entry
with self.path.open("a", encoding="utf-8") as f:
f.write(entry.to_jsonl())
def __contains__(self, key: CacheKey) -> bool:
return key.to_hash() in self._index
def __len__(self) -> int:
return len(self._index)
def iter_entries(self) -> Iterator[CacheEntry]:
yield from self._index.values()
def deterministic_seed(*parts: str | int) -> int:
"""Per experiment_matrix.md §0: seed deterministic per cell tuple.
Used to derive the seed from (persona_id, item_id, condition, architecture).
"""
payload = "|".join(str(p) for p in parts).encode("utf-8")
return int.from_bytes(hashlib.sha256(payload).digest()[:4], "big")