File size: 7,562 Bytes
411f347 a7c4301 b8e5043 a7c4301 411f347 a7c4301 b8e5043 a7c4301 411f347 b8e5043 a7c4301 b8e5043 411f347 a7c4301 6d49dc7 b8e5043 a7c4301 b8e5043 411f347 a7c4301 b8e5043 a7c4301 411f347 a7c4301 411f347 a7c4301 b8e5043 411f347 b8e5043 a7c4301 b8e5043 a7c4301 b8e5043 a7c4301 411f347 b8e5043 411f347 a7c4301 b8e5043 411f347 b8e5043 411f347 b8e5043 411f347 b8e5043 411f347 b8e5043 411f347 b8e5043 411f347 b8e5043 705e71e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | """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)
|