memory_store.py

script

← Back to skill

Content hash: b6ceb32a6c166224fa92302f124bc94d1ea1be1d29c17dbe566ece24551c231a
#!/usr/bin/env python3
"""Four-tier agent memory store — working, episodic, semantic, procedural.

Demonstrates: in-memory store with write, consolidate, recall, and staleness handling.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field


@dataclass
class MemoryEntry:
    content: str
    timestamp: float = field(default_factory=time.time)
    source_session: str = ""
    superseded: bool = False


class AgentMemory:
    """Four-tier memory: working, episodic, semantic, procedural."""

    def __init__(self):
        self.working: list[dict] = []
        self.episodic: list[MemoryEntry] = []
        self.semantic: dict[str, MemoryEntry] = {}
        self.procedural: dict[str, str] = {}
        self.session_id: str = ""

    def start_session(self, session_id: str):
        self.session_id = session_id
        self.working = []

    def observe(self, event: str):
        self.working.append({"ts": time.time(), "event": event})

    def end_session(self):
        for entry in self.working:
            self.episodic.append(
                MemoryEntry(content=entry["event"], timestamp=entry["ts"],
                            source_session=self.session_id)
            )

    def consolidate(self, extract_func):
        """Run extraction function over recent episodes to produce semantic facts."""
        recent = [e for e in self.episodic if e.source_session == self.session_id]
        facts = extract_func(recent)
        for key, value in facts.items():
            self._upsert_semantic(key, value)

    def _upsert_semantic(self, key: str, value: str):
        """Insert or update a fact; supersede old value on conflict."""
        if key in self.semantic:
            self.semantic[key].superseded = True
        self.semantic[key] = MemoryEntry(content=value)

    def recall_semantic(self, query_keys: list[str]) -> dict[str, str]:
        return {
            k: self.semantic[k].content
            for k in query_keys
            if k in self.semantic and not self.semantic[k].superseded
        }

    def recall_recent_episodes(self, n: int = 5) -> list[str]:
        return [e.content for e in self.episodic[-n:]]

    def add_procedure(self, name: str, policy: str, version: int = 1):
        self.procedural[f"{name}@v{version}"] = policy

    def get_procedure(self, name: str) -> str | None:
        versions = sorted(
            [k for k in self.procedural if k.startswith(f"{name}@v")], reverse=True)
        return self.procedural[versions[0]] if versions else None

    def prune_episodic(self, max_age_seconds: float):
        cutoff = time.time() - max_age_seconds
        self.episodic = [e for e in self.episodic if e.timestamp > cutoff]

    def prune_working(self, keep_last: int = 10):
        if len(self.working) > keep_last:
            self.working = self.working[-keep_last:]


def demo_extract(episodes: list[MemoryEntry]) -> dict[str, str]:
    """Mock consolidation: extract user preferences from episodes."""
    text = " ".join(e.content for e in episodes)
    facts = {}
    if "kebab" in text.lower():
        facts["naming_preference"] = "kebab-case"
    if "deploy" in text.lower():
        facts["deploy_status"] = "paused this week"
    return facts


if __name__ == "__main__":
    mem = AgentMemory()

    # Session 1
    mem.start_session("s1")
    mem.observe("User asked about function names")
    mem.observe("I prefer kebab-case for all my functions")
    mem.observe("Deploy is paused this week")
    mem.end_session()
    mem.consolidate(demo_extract)

    print("After session 1 — facts:", mem.recall_semantic(["naming_preference", "deploy_status"]))

    # Session 2 — recall context
    mem.start_session("s2")
    context = mem.recall_semantic(["naming_preference", "deploy_status"])
    print("Session 2 recalled:", context)
    assert context.get("naming_preference") == "kebab-case"

    # Contradiction handling
    mem.observe("Actually, I switched to snake_case")
    mem.end_session()
    mem._upsert_semantic("naming_preference", "snake_case")
    print("After update — naming:", mem.recall_semantic(["naming_preference"]))
    assert mem.semantic["naming_preference"].content == "snake_case"
    print("✓ All assertions passed")