| """ |
| Associative Memory (A-Mem) β graph-based, Zettelkasten-inspired. |
| |
| Each node is a MemoryItem enriched with keywords, metadata tags, and |
| contextual relationships. New nodes retroactively rewire existing links |
| based on semantic overlap, mimicking the A-Mem self-wiring mechanism. |
| """ |
| from datetime import datetime, timezone |
| from typing import Dict, List, Optional, Set, Tuple |
| from .models import MemoryItem, MemoryTier, RetrievalResult |
| from .retriever import retrieve |
|
|
|
|
| class AssociativeMemory: |
| def __init__(self): |
| self._nodes: Dict[str, MemoryItem] = {} |
| |
| self._edges: Dict[str, List[Tuple[str, str]]] = {} |
|
|
| |
|
|
| def add(self, item: MemoryItem) -> MemoryItem: |
| item.tier = MemoryTier.ASSOCIATIVE |
| self._nodes[item.item_id] = item |
| self._edges.setdefault(item.item_id, []) |
| self._auto_wire(item) |
| return item |
|
|
| def get(self, item_id: str) -> Optional[MemoryItem]: |
| return self._nodes.get(item_id) |
|
|
| def all(self) -> List[MemoryItem]: |
| return list(self._nodes.values()) |
|
|
| def remove(self, item_id: str) -> None: |
| self._nodes.pop(item_id, None) |
| self._edges.pop(item_id, None) |
| for edges in self._edges.values(): |
| edges[:] = [(rid, rel) for rid, rel in edges if rid != item_id] |
|
|
| |
|
|
| def link(self, source_id: str, target_id: str, relation: str = "related") -> None: |
| if source_id in self._nodes and target_id in self._nodes: |
| self._edges[source_id].append((target_id, relation)) |
|
|
| def neighbors(self, item_id: str) -> List[Tuple[MemoryItem, str]]: |
| return [ |
| (self._nodes[rid], rel) |
| for rid, rel in self._edges.get(item_id, []) |
| if rid in self._nodes |
| ] |
|
|
| |
|
|
| def retrieve(self, query: str, top_k: int = 5) -> List[RetrievalResult]: |
| results = retrieve(query, self.all(), top_k=top_k) |
| for r in results: |
| r.item.usage_count += 1 |
| r.item.updated_at = datetime.now(timezone.utc) |
| return results |
|
|
| |
|
|
| def _auto_wire(self, new_item: MemoryItem, threshold: float = 0.2) -> None: |
| query = new_item.content + " " + " ".join(new_item.tags) |
| candidates = [n for n in self._nodes.values() if n.item_id != new_item.item_id] |
| if not candidates: |
| return |
| from .retriever import score_item |
| max_u = max((c.usage_count for c in candidates), default=1) or 1 |
| for node in candidates: |
| result = score_item(query, node, max_u) |
| if result.similarity >= threshold: |
| self.link(new_item.item_id, node.item_id, "semantic_link") |
|
|