Entelechy / memory /storage.py
qa296
refactor: standardize type hints and improve null safety across codebase
6d49dc7
"""Markdown file storage for memories with CORE.md + category folders."""
import aiofiles
from datetime import datetime
from pathlib import Path
import yaml
class MemoryStorage:
"""Memory storage: CORE.md + category folders."""
def __init__(self, base_path: Path):
self.base_path = Path(base_path)
self.core_path = self.base_path / "CORE.md"
self._ensure_dirs()
def _ensure_dirs(self):
"""Create necessary directories."""
self.base_path.mkdir(parents=True, exist_ok=True)
# Create CORE.md if it doesn't exist
if not self.core_path.exists():
self.core_path.write_text(
"# 核心记忆(最高优先级)\n\n"
"这个文件存放最重要的信息,每次LLM调用时都会注入。\n\n"
"## 核心原则\n\n"
"### 诚实原则\n\n"
"**记忆诚实:**\n\n"
"当你写入记忆时:\n"
"- 只记录**真实发生**的事情\n"
"\n"
)
async def store(self, content: str, category: str | None = None) -> Path:
"""Store a memory as a markdown file with frontmatter.
Args:
content: The markdown content to store.
category: Optional category folder (e.g., "python", "ai", "project-x").
Returns:
The path to the created file.
"""
if category:
# Store in category folder
category_path = self.base_path / category
category_path.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_path = category_path / f"{timestamp}.md"
else:
# Store in root memory directory
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_path = self.base_path / f"{timestamp}.md"
frontmatter = {
"timestamp": datetime.now().isoformat(),
"category": category,
}
text = self._format_with_frontmatter(frontmatter, content)
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
await f.write(text)
return file_path
async def load_core(self) -> str:
"""Load the highest priority CORE.md memory."""
if self.core_path.exists():
return self.core_path.read_text(encoding="utf-8")
return ""
async def load_category(self, category: str) -> str:
"""Load all memories from a category folder."""
category_path = self.base_path / category
if not category_path.exists():
return ""
parts = []
for md_file in sorted(category_path.rglob("*.md")):
async with aiofiles.open(md_file, "r", encoding="utf-8") as f:
parts.append(await f.read())
return "\n\n---\n\n".join(parts)
async def list_categories(self) -> list[str]:
"""List all category folders."""
categories = []
for item in self.base_path.iterdir():
if item.is_dir() and item.name not in {".git", "__pycache__"}:
categories.append(item.name)
return sorted(categories)
async def list_memories(self, category: str | None = None) -> list[dict]:
"""List all stored memories with their metadata."""
results = []
if category:
# List specific category
category_path = self.base_path / category
if category_path.exists():
for md_file in sorted(category_path.rglob("*.md")):
results.append(await self._load_memory_metadata(md_file, category))
else:
# List all categories + root memories
# Core memory
if self.core_path.exists():
results.append({
"path": str(self.core_path),
"category": "CORE",
"is_core": True,
"preview": self.core_path.read_text()[:200],
})
# Category folders
for cat in await self.list_categories():
category_path = self.base_path / cat
for md_file in sorted(category_path.rglob("*.md")):
results.append(await self._load_memory_metadata(md_file, cat))
return results
async def _load_memory_metadata(self, file_path: Path, category: str) -> dict:
"""Load metadata and preview from a memory file."""
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
content = await f.read()
frontmatter, body = self._parse_frontmatter(content)
preview = body[:200] if len(body) > 200 else body
return {
"path": str(file_path),
"category": category,
"is_core": False,
"metadata": frontmatter,
"preview": preview,
}
@staticmethod
def _format_with_frontmatter(frontmatter: dict, content: str) -> str:
fm_text = yaml.dump(frontmatter, allow_unicode=True, default_flow_style=False).strip()
return f"---\n{fm_text}\n---\n\n{content}"
@staticmethod
def _parse_frontmatter(text: str) -> tuple[dict, str]:
"""Parse YAML frontmatter from markdown text."""
if not text.startswith("---"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
try:
fm = yaml.safe_load(parts[1]) or {}
except yaml.YAMLError:
fm = {}
body = parts[2].strip()
return fm, body
async def journal(self, content: str) -> Path:
"""Write a journal entry to today's daily journal file.
Args:
content: The journal entry content.
Returns:
The path to the journal file.
"""
journal_dir = self.base_path / "journal"
journal_dir.mkdir(parents=True, exist_ok=True)
today = datetime.now().strftime("%Y-%m-%d")
journal_path = journal_dir / f"{today}.md"
# Append to existing journal or create new one
timestamp = datetime.now().strftime("%H:%M:%S")
entry = f"\n\n## {timestamp}\n\n{content}\n"
if journal_path.exists():
async with aiofiles.open(journal_path, "a", encoding="utf-8") as f:
await f.write(entry)
else:
frontmatter = {
"date": today,
"type": "journal",
}
text = self._format_with_frontmatter(frontmatter, f"# Journal - {today}\n{entry}")
async with aiofiles.open(journal_path, "w", encoding="utf-8") as f:
await f.write(text)
return journal_path
async def load_recent_journal(self, days: int = 3) -> str:
"""Load recent journal entries.
Args:
days: Number of days to look back.
Returns:
Combined journal entries.
"""
journal_dir = self.base_path / "journal"
if not journal_dir.exists():
return ""
from datetime import timedelta
parts = []
for i in range(days):
date = datetime.now() - timedelta(days=i)
journal_path = journal_dir / f"{date.strftime('%Y-%m-%d')}.md"
if journal_path.exists():
async with aiofiles.open(journal_path, "r", encoding="utf-8") as f:
parts.append(await f.read())
return "\n\n---\n\n".join(parts)