Upload mythos/memory.py
Browse files- mythos/memory.py +134 -0
mythos/memory.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Memory system for Mythos agents — MIRIX-inspired 4-component taxonomy."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
from enum import Enum
|
| 8 |
+
from typing import Any, Optional
|
| 9 |
+
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class MemoryType(str, Enum):
|
| 14 |
+
"""Types of memory in the MIRIX taxonomy."""
|
| 15 |
+
|
| 16 |
+
CORE = "core" # Identity, persona, values
|
| 17 |
+
EPISODIC = "episodic" # Timestamped events, experiences
|
| 18 |
+
SEMANTIC = "semantic" # Facts, knowledge, concepts
|
| 19 |
+
PROCEDURAL = "procedural" # Workflows, skills, how-to
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class MemoryEntry(BaseModel):
|
| 23 |
+
"""A single memory entry."""
|
| 24 |
+
|
| 25 |
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 26 |
+
type: MemoryType
|
| 27 |
+
content: str
|
| 28 |
+
agent: str # Which agent owns this memory
|
| 29 |
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 30 |
+
importance: float = 0.5 # 0.0 to 1.0
|
| 31 |
+
tags: list[str] = Field(default_factory=list)
|
| 32 |
+
source: Optional[str] = None # Where did this memory come from
|
| 33 |
+
access_count: int = 0 # For LRU-style eviction
|
| 34 |
+
last_accessed: Optional[datetime] = None
|
| 35 |
+
embedding: Optional[list[float]] = None # For vector search
|
| 36 |
+
|
| 37 |
+
def touch(self) -> None:
|
| 38 |
+
"""Mark memory as accessed."""
|
| 39 |
+
self.access_count += 1
|
| 40 |
+
self.last_accessed = datetime.now(timezone.utc)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Memory(BaseModel):
|
| 44 |
+
"""Agent memory store with Active Retrieval."""
|
| 45 |
+
|
| 46 |
+
agent_name: str
|
| 47 |
+
entries: list[MemoryEntry] = Field(default_factory=list)
|
| 48 |
+
max_entries: int = 1000
|
| 49 |
+
auto_retrieve: bool = True
|
| 50 |
+
|
| 51 |
+
def add(
|
| 52 |
+
self,
|
| 53 |
+
content: str,
|
| 54 |
+
mem_type: MemoryType,
|
| 55 |
+
importance: float = 0.5,
|
| 56 |
+
tags: Optional[list[str]] = None,
|
| 57 |
+
source: Optional[str] = None,
|
| 58 |
+
) -> MemoryEntry:
|
| 59 |
+
"""Add a new memory entry."""
|
| 60 |
+
entry = MemoryEntry(
|
| 61 |
+
type=mem_type,
|
| 62 |
+
content=content,
|
| 63 |
+
agent=self.agent_name,
|
| 64 |
+
importance=importance,
|
| 65 |
+
tags=tags or [],
|
| 66 |
+
source=source,
|
| 67 |
+
)
|
| 68 |
+
self.entries.append(entry)
|
| 69 |
+
if len(self.entries) > self.max_entries:
|
| 70 |
+
self._evict_least_important()
|
| 71 |
+
return entry
|
| 72 |
+
|
| 73 |
+
def retrieve(
|
| 74 |
+
self,
|
| 75 |
+
query: str,
|
| 76 |
+
mem_type: Optional[MemoryType] = None,
|
| 77 |
+
limit: int = 5,
|
| 78 |
+
) -> list[MemoryEntry]:
|
| 79 |
+
"""Retrieve relevant memories. Simple keyword match; override for vector search."""
|
| 80 |
+
query_lower = query.lower()
|
| 81 |
+
candidates = self.entries
|
| 82 |
+
if mem_type:
|
| 83 |
+
candidates = [e for e in candidates if e.type == mem_type]
|
| 84 |
+
|
| 85 |
+
scored = []
|
| 86 |
+
for entry in candidates:
|
| 87 |
+
score = 0.0
|
| 88 |
+
if query_lower in entry.content.lower():
|
| 89 |
+
score += 1.0
|
| 90 |
+
score += entry.importance * 0.5
|
| 91 |
+
score += min(entry.access_count * 0.01, 0.3)
|
| 92 |
+
scored.append((score, entry))
|
| 93 |
+
|
| 94 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 95 |
+
results = [entry for _, entry in scored[:limit]]
|
| 96 |
+
for entry in results:
|
| 97 |
+
entry.touch()
|
| 98 |
+
return results
|
| 99 |
+
|
| 100 |
+
def active_retrieve(self, context: str) -> list[MemoryEntry]:
|
| 101 |
+
"""Infer topics from context and retrieve relevant memories automatically."""
|
| 102 |
+
# Simple topic extraction: split into words, filter short ones
|
| 103 |
+
words = [w.lower() for w in context.split() if len(w) > 4]
|
| 104 |
+
topics = list(set(words))[:5]
|
| 105 |
+
|
| 106 |
+
all_results: list[MemoryEntry] = []
|
| 107 |
+
for topic in topics:
|
| 108 |
+
all_results.extend(self.retrieve(topic, limit=3))
|
| 109 |
+
|
| 110 |
+
# Deduplicate by id
|
| 111 |
+
seen = set()
|
| 112 |
+
deduped = []
|
| 113 |
+
for entry in all_results:
|
| 114 |
+
if entry.id not in seen:
|
| 115 |
+
seen.add(entry.id)
|
| 116 |
+
deduped.append(entry)
|
| 117 |
+
|
| 118 |
+
deduped.sort(key=lambda e: e.importance, reverse=True)
|
| 119 |
+
return deduped[:10]
|
| 120 |
+
|
| 121 |
+
def _evict_least_important(self) -> None:
|
| 122 |
+
"""Remove least important / least accessed memory."""
|
| 123 |
+
self.entries.sort(
|
| 124 |
+
key=lambda e: (e.importance, e.access_count, e.timestamp),
|
| 125 |
+
)
|
| 126 |
+
self.entries.pop(0)
|
| 127 |
+
|
| 128 |
+
def to_context(self, entries: Optional[list[MemoryEntry]] = None) -> str:
|
| 129 |
+
"""Format memories as context string for LLM prompt."""
|
| 130 |
+
entries = entries or self.entries[-20:]
|
| 131 |
+
lines = []
|
| 132 |
+
for entry in entries:
|
| 133 |
+
lines.append(f"[{entry.type.value.upper()}] {entry.content}")
|
| 134 |
+
return "\n".join(lines)
|