Skip to content

Memory module proposition - v2 #2744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions mesa/experimental/memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Core event management functionality for Mesa's memory system.

This module provides the foundational data structures and classes needed for memory
entries recording in mesa. The Memory class is a manager between the ShortTermMemory
(dque data structure) and LongTermMemory (hash map).


The module now contains four main component:
- Memory: The operating class for managing ShortTermMemory and LongTermMemory
- ShortTermMemory more memory-efficient and reactive (efficient store and pop functionality)
- LongTermMemory : more computational-efficient (efficient navigation)
"""

from .memory import LongTermMemory, Memory, MemoryEntry, ShortTermMemory

Check warning on line 14 in mesa/experimental/memory/__init__.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/__init__.py#L14

Added line #L14 was not covered by tests

__all__ = [

Check warning on line 16 in mesa/experimental/memory/__init__.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/__init__.py#L16

Added line #L16 was not covered by tests
"LongTermMemory",
"Memory",
"MemoryEntry",
"ShortTermMemory",
]
286 changes: 286 additions & 0 deletions mesa/experimental/memory/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
"""An entry-recording functionality designed to be used as the memory of an agent.

This module provides the foundational class needed for storing information in the memory of an agent.
Then objective is to implement a very simple and efficient system that can be used for any agent and
for any kind of information (an entry). The user defines the capacity of the memory chooses the format
of the entries, which allows for a greater flexibility.

Features:

- Capacity-based short-term memory, with a FIFO system.
- Efficient storage and retrieval of entries.
- Support for different entry types of entries.
- Possibility to send entries to other agents.

For now, the module contains only one main component:
- Memory: A class representing the memory of an agent.
"""

import copy
from collections import deque
from typing import Any

Check warning on line 21 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L19-L21

Added lines #L19 - L21 were not covered by tests


class MemoryEntry:

Check warning on line 24 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L24

Added line #L24 was not covered by tests
"""Base class for all memory entries."""

def __init__(

Check warning on line 27 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L27

Added line #L27 was not covered by tests
self,
entry_content: str,
entry_step: int,
entry_type: str,
entry_metadata: dict | None = None,
):
"""Initialize a MemoryEntry with given content, step, type, and optional metadata."""
self.entry_content = entry_content
self.entry_step = entry_step
self.entry_type = entry_type
self.entry_metadata = entry_metadata or {}

Check warning on line 38 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L35-L38

Added lines #L35 - L38 were not covered by tests

def to_dict(self) -> dict:

Check warning on line 40 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L40

Added line #L40 was not covered by tests
"""Convert memory entry to dictionary for serialization."""
return {

Check warning on line 42 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L42

Added line #L42 was not covered by tests
"entry_content": self.entry_content,
"entry_step": self.entry_step,
"entry_type": self.entry_type,
"entry_metadata": self.entry_metadata,
}

@classmethod
def from_dict(cls, data: dict) -> "MemoryEntry":

Check warning on line 50 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L49-L50

Added lines #L49 - L50 were not covered by tests
"""Create memory entry from dictionary."""
entry = cls(

Check warning on line 52 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L52

Added line #L52 was not covered by tests
entry_content=data["entry_content"],
entry_step=data["entry_step"],
entry_type=data["entry_type"],
entry_metadata=data["entry_metadata"],
)
return entry

Check warning on line 58 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L58

Added line #L58 was not covered by tests


class ShortTermMemory:

Check warning on line 61 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L61

Added line #L61 was not covered by tests
"""Short-term memory with limited capacity that follows recency principles.

Implemented as a double-ended queue with O(1) add/remove operations.
"""

def __init__(self, model, capacity: int = 10):

Check warning on line 67 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L67

Added line #L67 was not covered by tests
"""Initialize ShortTermMemory with a model and capacity."""
self.model = model
self.capacity = capacity
self.entries = deque(maxlen=capacity)

Check warning on line 71 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L69-L71

Added lines #L69 - L71 were not covered by tests

def add(

Check warning on line 73 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L73

Added line #L73 was not covered by tests
self,
model,
entry_content: str | None = None,
entry_type: str = "general",
entry_metadata: dict | None = None,
entry=None,
) -> MemoryEntry:
"""Add a new entry to short-term memory."""
if entry is not None:
self.entries.append(entry)
return entry

Check warning on line 84 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L83-L84

Added lines #L83 - L84 were not covered by tests

entry_metadata = entry_metadata or {}
entry = MemoryEntry(

Check warning on line 87 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L86-L87

Added lines #L86 - L87 were not covered by tests
entry_step=model.step,
entry_content=entry_content,
entry_type=entry_type,
entry_metadata=entry_metadata,
)
self.entries.append(entry)
return entry

Check warning on line 94 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L93-L94

Added lines #L93 - L94 were not covered by tests

def get_recent(self, n: int = 10) -> list[MemoryEntry]:

Check warning on line 96 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L96

Added line #L96 was not covered by tests
"""Get n most recent entries."""
return list(self.entries)[-n:]

Check warning on line 98 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L98

Added line #L98 was not covered by tests

def get_by_id(self, entry_id) -> MemoryEntry | None:

Check warning on line 100 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L100

Added line #L100 was not covered by tests
"""Retrieve an entry by its ID."""
entry_list = [entry for entry in self.entries if id(entry) == entry_id]

Check warning on line 102 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L102

Added line #L102 was not covered by tests
if entry_list:
return entry_list[0]

Check warning on line 104 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L104

Added line #L104 was not covered by tests
else:
return None

Check warning on line 106 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L106

Added line #L106 was not covered by tests

def get_by_type(self, entry_type: str) -> list[MemoryEntry]:

Check warning on line 108 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L108

Added line #L108 was not covered by tests
"""Get entries from a specific entry type."""
entry_list = [entry for entry in self.entries if entry.entry_type == entry_type]
return entry_list

Check warning on line 111 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L110-L111

Added lines #L110 - L111 were not covered by tests

def forget_last(self) -> bool:

Check warning on line 113 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L113

Added line #L113 was not covered by tests
"""Remove the most recent entry from memory. Returns True if successful."""
if self.entries:
self.entries.pop()
return True

Check warning on line 117 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L116-L117

Added lines #L116 - L117 were not covered by tests
else:
return False

Check warning on line 119 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L119

Added line #L119 was not covered by tests

def forget_first(self) -> bool:

Check warning on line 121 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L121

Added line #L121 was not covered by tests
"""Remove the oldest entry from memory. Returns True if successful."""
if self.entries:
self.entries.popleft()
return True

Check warning on line 125 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L124-L125

Added lines #L124 - L125 were not covered by tests
else:
return False

Check warning on line 127 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L127

Added line #L127 was not covered by tests

def forget(self, entry_id=None, entry: MemoryEntry = None) -> bool:

Check warning on line 129 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L129

Added line #L129 was not covered by tests
"""Remove an entry from short-term memory."""
if entry_id is not None:
entry_list = [entry for entry in self.entries if id(entry) == entry_id]

Check warning on line 132 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L132

Added line #L132 was not covered by tests
if entry_list:
entry = entry_list[0]

Check warning on line 134 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L134

Added line #L134 was not covered by tests
if isinstance(entry, MemoryEntry):
try:
self.entries.remove(entry)
return True
except ValueError:
return False
return False

Check warning on line 141 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L136-L141

Added lines #L136 - L141 were not covered by tests

def clear(self):

Check warning on line 143 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L143

Added line #L143 was not covered by tests
"""Remove all entries from memory."""
self.entries.clear()

Check warning on line 145 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L145

Added line #L145 was not covered by tests


class LongTermMemory:

Check warning on line 148 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L148

Added line #L148 was not covered by tests
"""Long-term memory with categorization and importance-based retrieval.

Implemented using dictionaries for O(1) entry type access.
"""

def __init__(self, model):

Check warning on line 154 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L154

Added line #L154 was not covered by tests
"""Initialize LongTermMemory with a model."""
self.model = model
self.entries: dict[int, MemoryEntry] = {}

Check warning on line 157 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L156-L157

Added lines #L156 - L157 were not covered by tests

def add(

Check warning on line 159 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L159

Added line #L159 was not covered by tests
self,
model,
entry_content: str | None = None,
entry_type: str = "general",
entry_metadata: dict | None = None,
entry=None,
) -> MemoryEntry:
"""Add a new entry to long-term memory."""
if entry is not None:
entry_id = id(entry)
self.entries[entry_id] = entry
return entry

Check warning on line 171 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L169-L171

Added lines #L169 - L171 were not covered by tests

entry_metadata = entry_metadata or {}
entry = MemoryEntry(

Check warning on line 174 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L173-L174

Added lines #L173 - L174 were not covered by tests
entry_step=model.step,
entry_content=entry_content,
entry_type=entry_type,
entry_metadata=entry_metadata,
)
entry_id = id(entry)
self.entries[entry_id] = entry
return entry

Check warning on line 182 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L180-L182

Added lines #L180 - L182 were not covered by tests

def get_by_id(self, entry_id) -> MemoryEntry | None:

Check warning on line 184 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L184

Added line #L184 was not covered by tests
"""Retrieve an entry by its ID."""
if entry_id in self.entries:
return self.entries[entry_id]
return None

Check warning on line 188 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L187-L188

Added lines #L187 - L188 were not covered by tests

def get_by_type(self, entry_type: str) -> list[MemoryEntry]:

Check warning on line 190 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L190

Added line #L190 was not covered by tests
"""Get entries from a specific entry type."""
entry_list = [

Check warning on line 192 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L192

Added line #L192 was not covered by tests
entry for entry in self.entries.values() if entry.entry_type == entry_type
]
return entry_list

Check warning on line 195 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L195

Added line #L195 was not covered by tests

def forget(self, entry_id=None, entry: MemoryEntry = None) -> bool:

Check warning on line 197 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L197

Added line #L197 was not covered by tests
"""Remove an entry from long-term memory."""
if entry:
entry_id = id(entry)

Check warning on line 200 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L200

Added line #L200 was not covered by tests
if entry_id is None or entry_id not in self.entries:
return False
del self.entries[entry_id]
return True

Check warning on line 204 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L202-L204

Added lines #L202 - L204 were not covered by tests


class Memory:

Check warning on line 207 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L207

Added line #L207 was not covered by tests
"""Main memory manager combining short-term and long-term memory.

Provides consolidation, search (by type for now) and memory transfer (communicate) functionality.
"""

def __init__(self, agent, model, stm_capacity: int = 10):

Check warning on line 213 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L213

Added line #L213 was not covered by tests
"""Initialize Memory with an agent, model, and short-term memory capacity."""
self.model = model
self.agent = agent
self.short_term = ShortTermMemory(model=self.model, capacity=stm_capacity)
self.long_term = LongTermMemory(model=self.model)

Check warning on line 218 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L215-L218

Added lines #L215 - L218 were not covered by tests

def remember_short_term(

Check warning on line 220 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L220

Added line #L220 was not covered by tests
self,
model,
entry_content: Any,
entry_type: str = "general",
entry_metadata: dict | None = None,
) -> MemoryEntry:
"""Add an entry to short-term memory. Returns the MemoryEntry object created."""
return self.short_term.add(model, entry_content, entry_type, entry_metadata)

Check warning on line 228 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L228

Added line #L228 was not covered by tests

def remember_long_term(

Check warning on line 230 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L230

Added line #L230 was not covered by tests
self,
model,
entry_content: Any,
entry_type: str = "general",
entry_metadata: dict | None = None,
) -> MemoryEntry:
"""Add an entry directly to long-term memory."""
return self.long_term.add(model, entry_content, entry_type, entry_metadata)

Check warning on line 238 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L238

Added line #L238 was not covered by tests

def consolidate(self, entry: MemoryEntry) -> MemoryEntry:

Check warning on line 240 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L240

Added line #L240 was not covered by tests
"""Transfer an entry from short-term to long-term memory.

Returns the MemoryEntry object transferred.
"""
entry = self.long_term.add(entry=entry, model=self.model)
self.short_term.forget(entry_id=id(entry))
return entry

Check warning on line 247 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L245-L247

Added lines #L245 - L247 were not covered by tests

def get_by_type(

Check warning on line 249 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L249

Added line #L249 was not covered by tests
self,
entry_type: str,
include_short_term: bool = True,
include_long_term: bool = True,
limit: int = 10,
) -> list[MemoryEntry]:
"""Get a list of entries of the same entry type."""
results: list[MemoryEntry] = []

Check warning on line 257 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L257

Added line #L257 was not covered by tests
if include_short_term:
short_results = self.short_term.get_by_type(entry_type)

Check warning on line 259 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L259

Added line #L259 was not covered by tests
if short_results is not None:
if isinstance(short_results, list):
results.extend(short_results)

Check warning on line 262 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L262

Added line #L262 was not covered by tests
else:
results.append(short_results)

Check warning on line 264 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L264

Added line #L264 was not covered by tests
if include_long_term:
long_results = self.long_term.get_by_type(entry_type)

Check warning on line 266 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L266

Added line #L266 was not covered by tests
if long_results is not None:
if isinstance(long_results, list):
results.extend(long_results)

Check warning on line 269 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L269

Added line #L269 was not covered by tests
else:
results.append(long_results)

Check warning on line 271 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L271

Added line #L271 was not covered by tests
if not results:
return []
return results[:limit] if limit is not None else results

Check warning on line 274 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L273-L274

Added lines #L273 - L274 were not covered by tests

def communicate(self, entry, external_agent):

Check warning on line 276 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L276

Added line #L276 was not covered by tests
"""Send a memory entry to another agent by making a deep copy of the entry."""
entry_copy = copy.deepcopy(entry)
entry_copy.entry_metadata["external_id"] = self.agent.unique_id
new_entry = external_agent.memory.remember_short_term(

Check warning on line 280 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L278-L280

Added lines #L278 - L280 were not covered by tests
model=self.model,
entry_content=entry_copy.entry_content,
entry_type=entry_copy.entry_type,
entry_metadata=entry_copy.entry_metadata,
)
return new_entry

Check warning on line 286 in mesa/experimental/memory/memory.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/memory/memory.py#L286

Added line #L286 was not covered by tests
Loading