Download code_release/code/memebench/methods/base.py from clinical-agent-memory-bench/meme-bench: direct link, hf CLI and curl.
- Browser
- Download file 8.82 kB
-
https://huggingface.co/spaces/clinical-agent-memory-bench/meme-bench/resolve/main/code_release/code/memebench/methods/base.py
- Command line
-
hf download hf://spaces/clinical-agent-memory-bench/meme-bench/code_release/code/memebench/methods/base.py
-
curl -L -o base.py https://huggingface.co/spaces/clinical-agent-memory-bench/meme-bench/resolve/main/code_release/code/memebench/methods/base.py
8.82 kB
| """Base contract for memory methods. | |
| A method implements memorize() for each visit and build_memory_context() for each question. | |
| The runner owns the answering call, so every method is answered with the same prompt, except | |
| methods whose own agent produces the answer (ANSWERS_INTERNALLY). memorize() receives the | |
| rendered visit text with its visit number and date. | |
| """ | |
| from __future__ import annotations | |
| from abc import ABC, abstractmethod | |
| from dataclasses import dataclass, field | |
| def as_list(x) -> list: | |
| """Coerce anything list-like (a numpy array included) to a plain list.""" | |
| if x is None: | |
| return [] | |
| try: | |
| return list(x) | |
| except Exception: | |
| return [] | |
| class Tokenizer: | |
| """Token counter backed by the tiktoken encoding named in backbone.yaml (`answer.tokenizer` for context budgets | |
| and chunk sizes, `embedding.tokenizer` for embedding inputs); run_eval sets it before any method runs.""" | |
| def __init__(self, role: str): | |
| self.role, self._enc = role, None | |
| def set(self, name: str | None) -> None: | |
| if name and not (isinstance(name, str) and name.startswith("<")): | |
| import tiktoken | |
| self._enc = tiktoken.get_encoding(name) | |
| else: | |
| raise SystemExit(f"set {self.role}.tokenizer in backbone.yaml") | |
| def _get(self): | |
| if self._enc is None: | |
| raise RuntimeError(f"{self.role}.tokenizer is not set (backbone.yaml)") | |
| return self._enc | |
| def encode(self, text: str, **kw) -> list[int]: | |
| return self._get().encode(text, **kw) | |
| def decode(self, tokens) -> str: | |
| return self._get().decode(tokens) | |
| ENC = Tokenizer("answer") | |
| EMBED_ENC = Tokenizer("embedding") | |
| class MemoryBuildResult: | |
| """Result of one memorize() call.""" | |
| success: bool = True | |
| method: str = "" | |
| action: str = "" | |
| visit_number: int = -1 | |
| chunk_count: int = 0 | |
| added_chunks: int = 0 | |
| time_cost: float = 0.0 | |
| extra: dict = field(default_factory=dict) | |
| def to_dict(self) -> dict: | |
| return {"success": self.success, "method": self.method, "action": self.action, | |
| "visit_number": self.visit_number, "chunk_count": self.chunk_count, | |
| "added_chunks": self.added_chunks, "time_cost": round(self.time_cost, 4), | |
| **self.extra} | |
| class MemoryContext: | |
| """The MEMORY block handed to the runner, or the answer of an internally answering method.""" | |
| mem: str | |
| n_tok: int = 0 | |
| retrieved_count: int = 0 | |
| retrieved_visits: list = field(default_factory=list) | |
| retrieved_memories: list = field(default_factory=list) | |
| context_truncated: bool = False | |
| error: bool = False | |
| answer: str | None = None | |
| extra: dict = field(default_factory=dict) | |
| class BaseAgent(ABC): | |
| ANSWERS_INTERNALLY = False | |
| MEMORIZE_WITH_LLM = False | |
| def __init__(self, **params): | |
| self.params = params | |
| self._context_id: str | None = None | |
| def set_trace_sink(self, sink) -> None: | |
| """Install the callback that appends one event per dict to `memory_trace.jsonl`.""" | |
| self._trace_sink = sink | |
| def _trace(self, event: str, **fields) -> None: | |
| sink = getattr(self, "_trace_sink", None) | |
| if sink is None: | |
| return | |
| try: | |
| from ..llm_client import USAGE | |
| sink({"method": getattr(self, "METHOD_NAME", type(self).__name__), | |
| "event": event, "phase": getattr(USAGE, "phase", None), **fields}) | |
| except Exception as e: | |
| n = getattr(self, "_trace_fail", 0) + 1 | |
| self._trace_fail = n | |
| if n == 1: | |
| try: | |
| sink({"method": getattr(self, "METHOD_NAME", type(self).__name__), | |
| "event": "trace_error", "error": f"{type(e).__name__}: {e}"[:300], | |
| "failed_event": event}) | |
| except Exception: | |
| pass | |
| def consolidate(self) -> dict | None: | |
| """Sleep-time memory maintenance, run at each checkpoint before answering.""" | |
| return None | |
| def dump_memory(self, out_dir) -> dict | None: | |
| """Export the current memory state into out_dir; return a small stats dict, or None.""" | |
| return None | |
| def set_context_id(self, context_id: str) -> None: | |
| """Set the per-patient namespace.""" | |
| self._context_id = str(context_id) | |
| def memorize(self, text: str, visit_number: int, date: str) -> MemoryBuildResult: | |
| """Store one rendered visit into memory.""" | |
| def memorize_session(self, session: dict, | |
| rendered_text: str | None = None) -> MemoryBuildResult: | |
| """Memorize one session from dataset.load_sessions().""" | |
| if rendered_text is None: | |
| from ..dataset import render_visit | |
| rendered_text = render_visit(session) | |
| return self.memorize(rendered_text, session["visit_number"], session["date"]) | |
| def build_memory_context(self, question: str, as_of: str = "", | |
| display_question: str | None = None) -> MemoryContext: | |
| """Produce the MEMORY block for one question.""" | |
| def count_tokens(text: str) -> int: | |
| return len(ENC.encode(text)) if text else 0 | |
| def cut_tokens(text: str, max_tokens: int) -> str: | |
| """The first `max_tokens` tokens of `text`.""" | |
| toks = ENC.encode(text) | |
| return text if len(toks) <= max_tokens else ENC.decode(toks[:max_tokens]) | |
| def split_tokens(text: str, chunk_size: int, chunk_overlap: int, | |
| header: str = "") -> list[str]: | |
| """Split text into token windows; continuation chunks repeat `header` + '(cont.)'.""" | |
| if chunk_overlap >= chunk_size: | |
| raise ValueError(f"chunk_overlap ({chunk_overlap}) must be smaller " | |
| f"than chunk_size ({chunk_size})") | |
| if not text.strip(): | |
| return [] | |
| tokens = ENC.encode(text) | |
| if len(tokens) <= chunk_size: | |
| return [text] | |
| chunks, start = [], 0 | |
| while start < len(tokens): | |
| end = min(start + chunk_size, len(tokens)) | |
| piece = ENC.decode(tokens[start:end]) | |
| if start > 0 and header: | |
| piece = f"{header} (cont.)\n{piece}" | |
| chunks.append(piece) | |
| if end == len(tokens): | |
| break | |
| start = end - chunk_overlap | |
| return chunks | |
| def snapshot_jsonl_gz(path, rows) -> int: | |
| """Write rows as gzipped JSONL; returns row count.""" | |
| import gzip | |
| import json as _json | |
| with gzip.open(path, "wt", encoding="utf-8") as fh: | |
| n = 0 | |
| for r in rows: | |
| fh.write(_json.dumps(r, ensure_ascii=False, default=str) + "\n") | |
| n += 1 | |
| return n | |
| def snapshot_sqlite(src, dst) -> None: | |
| """Copy an open SQLite database with the backup API.""" | |
| import sqlite3 | |
| a = sqlite3.connect(f"file:{src}?mode=ro", uri=True) | |
| try: | |
| b = sqlite3.connect(str(dst)) | |
| try: | |
| with b: | |
| a.backup(b) | |
| finally: | |
| b.close() | |
| finally: | |
| a.close() | |
| def snapshot_vectors_delta(out_dir, pairs, seen: set) -> int: | |
| """Write the vectors this checkpoint added, and record them as seen.""" | |
| import numpy as np | |
| ids, vecs = [], [] | |
| for mid, v in pairs: | |
| mid = str(mid) | |
| if not mid or mid in seen or v is None: | |
| continue | |
| seen.add(mid) | |
| ids.append(mid) | |
| vecs.append(np.asarray(v, dtype="float32")) | |
| if not ids: | |
| np.savez_compressed(out_dir / "vectors.npz", | |
| ids=np.array([], dtype=object), vecs=np.zeros((0, 0), dtype="float32")) | |
| return 0 | |
| np.savez_compressed(out_dir / "vectors.npz", | |
| ids=np.array(ids, dtype=object), vecs=np.vstack(vecs)) | |
| return len(ids) | |
| def snapshot_tree(src, dst) -> int: | |
| """Copy a store directory, SQLite files through the backup API, skipping lock and tmp files; return the count.""" | |
| import shutil | |
| from pathlib import Path as _P | |
| src, dst = _P(src), _P(dst) | |
| n = 0 | |
| for f in src.rglob("*"): | |
| if not f.is_file() or f.name.endswith((".lock", "-wal", "-shm")): | |
| continue | |
| if "tmp" in f.relative_to(src).parts[:-1]: | |
| continue | |
| rel = dst / f.relative_to(src) | |
| rel.parent.mkdir(parents=True, exist_ok=True) | |
| if f.suffix in (".db", ".sqlite") or f.name.endswith(".sqlite3") or f.name == "storage.sqlite": | |
| try: | |
| snapshot_sqlite(f, rel) | |
| except Exception: | |
| shutil.copy2(f, rel) | |
| else: | |
| shutil.copy2(f, rel) | |
| n += 1 | |
| return n | |