Download src/ante/memory.py from Ransom070/Ante: direct link, hf CLI and curl.
- Browser
- Download file 6.3 kB
-
https://huggingface.co/spaces/Ransom070/Ante/resolve/main/src/ante/memory.py
- Command line
-
hf download hf://spaces/Ransom070/Ante/src/ante/memory.py
-
curl -L -o memory.py https://huggingface.co/spaces/Ransom070/Ante/resolve/main/src/ante/memory.py
6.3 kB
| from __future__ import annotations | |
| import os | |
| import json | |
| import time | |
| import random | |
| import string | |
| import logging | |
| import threading | |
| from pathlib import Path | |
| from dataclasses import dataclass | |
| from collections.abc import Mapping | |
| logger = logging.getLogger(__name__) | |
| SCHEMA_VERSION = 1 | |
| MAX_FACTS = 60 | |
| MAX_FACT_CHARS = 280 | |
| MEMORY_FILENAME = "memory.v1.json" | |
| _STORE_LOCK = threading.Lock() | |
| class MemoryFact: | |
| """One short long-term memory fact.""" | |
| id: str | |
| text: str | |
| created_at: int | |
| def to_json(self) -> dict[str, object]: | |
| """Return the persisted JSON shape used by the mobile app.""" | |
| return { | |
| "id": self.id, | |
| "text": self.text, | |
| "createdAt": self.created_at, | |
| } | |
| class ForgetMemoryResult: | |
| """Result of removing a memory fact.""" | |
| removed: MemoryFact | None | |
| candidates: tuple[MemoryFact, ...] | |
| def memory_path_for_instance(instance_path: str | Path | None = None) -> Path: | |
| """Return the memory JSON path for this app instance.""" | |
| if instance_path is not None: | |
| return Path(instance_path).expanduser() / MEMORY_FILENAME | |
| data_home = os.getenv("XDG_DATA_HOME") | |
| data_root = Path(data_home).expanduser() if data_home else Path.home() / ".local" / "share" | |
| return data_root / "ante" / MEMORY_FILENAME | |
| def normalize_memory_text(text: str) -> str: | |
| """Collapse whitespace and enforce the fact length cap.""" | |
| normalized = " ".join(text.split()).strip() | |
| if len(normalized) <= MAX_FACT_CHARS: | |
| return normalized | |
| return f"{normalized[: MAX_FACT_CHARS - 3]}..." | |
| def _make_id() -> str: | |
| suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) | |
| return f"m_{int(time.time() * 1000)}_{suffix}" | |
| def _now_ms() -> int: | |
| return int(time.time() * 1000) | |
| def _fact_from_json(value: object) -> MemoryFact | None: | |
| if not isinstance(value, Mapping): | |
| return None | |
| fact_id = value.get("id") | |
| text = value.get("text") | |
| created_at = value.get("createdAt") | |
| if not isinstance(fact_id, str): | |
| return None | |
| if not isinstance(text, str): | |
| return None | |
| if not isinstance(created_at, (int, float)): | |
| return None | |
| normalized = normalize_memory_text(text) | |
| if not normalized: | |
| return None | |
| return MemoryFact(id=fact_id, text=normalized, created_at=int(created_at)) | |
| def _read_memory_file(path: Path) -> list[MemoryFact]: | |
| try: | |
| raw = path.read_text(encoding="utf-8") | |
| except FileNotFoundError: | |
| return [] | |
| except OSError as exc: | |
| logger.warning("Failed to read memory store at %s: %s", path, exc) | |
| return [] | |
| try: | |
| parsed = json.loads(raw) | |
| except json.JSONDecodeError as exc: | |
| logger.warning("Failed to parse memory store at %s: %s", path, exc) | |
| return [] | |
| if not isinstance(parsed, Mapping): | |
| return [] | |
| facts_value = parsed.get("facts") | |
| if not isinstance(facts_value, list): | |
| return [] | |
| facts: list[MemoryFact] = [] | |
| for item in facts_value: | |
| fact = _fact_from_json(item) | |
| if fact is not None: | |
| facts.append(fact) | |
| return facts[:MAX_FACTS] | |
| def _write_memory_file(path: Path, facts: list[MemoryFact]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| payload = { | |
| "version": SCHEMA_VERSION, | |
| "facts": [fact.to_json() for fact in facts[:MAX_FACTS]], | |
| } | |
| tmp_path = path.with_name(f".{path.name}.{os.getpid()}.tmp") | |
| try: | |
| tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") | |
| tmp_path.replace(path) | |
| finally: | |
| try: | |
| tmp_path.unlink(missing_ok=True) | |
| except OSError: | |
| pass | |
| def list_memory_facts(instance_path: str | Path | None = None) -> list[MemoryFact]: | |
| """Return stored memory facts, newest first.""" | |
| with _STORE_LOCK: | |
| return list(_read_memory_file(memory_path_for_instance(instance_path))) | |
| def add_memory_fact(instance_path: str | Path | None, text: str) -> MemoryFact | None: | |
| """Store one short fact, deduplicating exact case-insensitive matches.""" | |
| normalized = normalize_memory_text(text) | |
| if not normalized: | |
| return None | |
| path = memory_path_for_instance(instance_path) | |
| with _STORE_LOCK: | |
| facts = _read_memory_file(path) | |
| existing = next((fact for fact in facts if fact.text.lower() == normalized.lower()), None) | |
| if existing is not None: | |
| return existing | |
| fact = MemoryFact(id=_make_id(), text=normalized, created_at=_now_ms()) | |
| _write_memory_file(path, [fact, *facts][:MAX_FACTS]) | |
| return fact | |
| def forget_memory_fact( | |
| instance_path: str | Path | None, | |
| *, | |
| query: str | None = None, | |
| ) -> ForgetMemoryResult: | |
| """Remove a fact by case-insensitive substring query.""" | |
| path = memory_path_for_instance(instance_path) | |
| with _STORE_LOCK: | |
| facts = _read_memory_file(path) | |
| normalized_query = normalize_memory_text(query or "").lower() | |
| if not normalized_query: | |
| return ForgetMemoryResult(removed=None, candidates=()) | |
| candidates = tuple(fact for fact in facts if normalized_query in fact.text.lower()) | |
| if not candidates: | |
| return ForgetMemoryResult(removed=None, candidates=()) | |
| removed = candidates[0] | |
| _write_memory_file(path, [fact for fact in facts if fact.id != removed.id]) | |
| return ForgetMemoryResult(removed=removed, candidates=candidates) | |
| def clear_memory_facts(instance_path: str | Path | None = None) -> None: | |
| """Remove all stored memory facts.""" | |
| path = memory_path_for_instance(instance_path) | |
| with _STORE_LOCK: | |
| _write_memory_file(path, []) | |
| def format_memory_for_prompt(instance_path: str | Path | None = None) -> str: | |
| """Return the prompt fragment injected before the session instructions.""" | |
| facts = list_memory_facts(instance_path) | |
| if not facts: | |
| return "" | |
| bullets = "\n".join(f"- {fact.text}" for fact in facts) | |
| return "\n".join( | |
| [ | |
| "Things you remember about the user (use this context naturally,", | |
| "do not recite the list verbatim):", | |
| bullets, | |
| ] | |
| ) | |