Skip to content

Commit 04bb99d

Browse files
authored
Refine basic memory example with pagination (#93)
1 parent 19b1e8d commit 04bb99d

File tree

5 files changed

+246
-0
lines changed

5 files changed

+246
-0
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This directory contains examples demonstrating how to use EnrichMCP.
1111
- [sqlalchemy_shop](sqlalchemy_shop) - SQLAlchemy ORM version
1212
- [shop_api_gateway](shop_api_gateway) - gateway in front of FastAPI
1313
- [mutable_crud](mutable_crud) - mutable fields and CRUD decorators
14+
- [basic_memory](basic_memory) - simple note-taking API using FileMemoryStore
1415
- [openai_chat_agent](openai_chat_agent) - interactive chat client
1516

1617
## Hello World

examples/basic_memory/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Basic Memory Example
2+
3+
This example ships with a tiny note storage implementation found in
4+
`memory.py`. Notes are stored as Markdown files with YAML front matter using
5+
`FileMemoryStore`. Everything lives in the `data` directory so you can open and
6+
edit the notes by hand. Listing notes only returns their IDs and titles with
7+
simple pagination.
8+
9+
```bash
10+
cd basic_memory
11+
python app.py
12+
```

examples/basic_memory/app.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Simple API demonstrating the in-memory note utilities.
2+
3+
This example exposes a tiny CRUD interface for notes grouped into a single
4+
``MemoryProject``. Notes are stored in ``./data`` using ``FileMemoryStore``
5+
which writes Markdown files with YAML front matter. Listing notes only returns
6+
their identifiers and titles while ``get_note`` returns the full content.
7+
"""
8+
9+
from pathlib import Path
10+
11+
# The example-specific storage utilities live next to this file so that the
12+
# main ``enrichmcp`` package remains lightweight.
13+
# Import the lightweight note storage utilities from the current directory.
14+
from memory import (
15+
FileMemoryStore,
16+
MemoryNote,
17+
MemoryNoteSummary,
18+
MemoryProject,
19+
)
20+
21+
from enrichmcp import EnrichMCP
22+
23+
store = FileMemoryStore(Path(__file__).parent / "data")
24+
project = MemoryProject("demo", store)
25+
26+
app = EnrichMCP(title="Basic Memory API", description="Manage simple notes")
27+
28+
29+
@app.entity
30+
class Note(MemoryNote):
31+
"""A note stored in the demo project."""
32+
33+
34+
@app.entity
35+
class NoteSummary(MemoryNoteSummary):
36+
"""Minimal note information returned from :func:`list_notes`."""
37+
38+
39+
@app.create
40+
async def create_note(title: str, content: str, tags: list[str] | None = None) -> Note:
41+
"""Create and persist a new note."""
42+
return project.create_note(title, content, tags)
43+
44+
45+
@app.retrieve
46+
async def get_note(note_id: str) -> Note:
47+
"""Retrieve a single note by its identifier."""
48+
note = project.get_note(note_id)
49+
if note is None:
50+
raise ValueError("note not found")
51+
return note
52+
53+
54+
@app.retrieve
55+
async def list_notes(page: int = 1, page_size: int = 10) -> list[NoteSummary]:
56+
"""Return a paginated list of notes."""
57+
return project.list_notes(page, page_size)
58+
59+
60+
if __name__ == "__main__":
61+
app.run()

examples/basic_memory/memory.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""Lightweight note storage used by the Basic Memory example.
2+
3+
The classes defined here implement a minimal project-based note system
4+
that persists notes to disk. Each note is stored as a Markdown file with
5+
YAML front matter so they can be easily edited outside of the example.
6+
This module intentionally keeps the logic simple and is not part of the
7+
public ``enrichmcp`` package. It merely supports the example API.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from abc import ABC, abstractmethod
13+
from typing import TYPE_CHECKING
14+
from uuid import uuid4
15+
16+
import yaml
17+
from pydantic import Field
18+
19+
from enrichmcp.entity import EnrichModel
20+
21+
if TYPE_CHECKING: # pragma: no cover - used only for type hints
22+
from pathlib import Path
23+
24+
25+
class MemoryNote(EnrichModel):
26+
"""A single note entry.
27+
28+
Parameters
29+
----------
30+
id:
31+
Unique identifier for the note. ``MemoryStore`` implementations
32+
generate this value when a note is created.
33+
title:
34+
Short title summarizing the note.
35+
content:
36+
The full body of the note.
37+
tags:
38+
Optional list of tag strings for free-form categorisation.
39+
"""
40+
41+
id: str = Field(description="Unique note identifier")
42+
title: str = Field(description="Note title")
43+
content: str = Field(description="Note content")
44+
tags: list[str] = Field(default_factory=list, description="Optional tags")
45+
46+
47+
class MemoryNoteSummary(EnrichModel):
48+
"""Lightweight representation containing only ``id`` and ``title``."""
49+
50+
id: str = Field(description="Unique note identifier")
51+
title: str = Field(description="Note title")
52+
53+
54+
class MemoryStore(ABC):
55+
"""Abstract storage backend for ``MemoryNote`` objects."""
56+
57+
@abstractmethod
58+
def new_id(self) -> str:
59+
"""Return a new unique note identifier."""
60+
61+
@abstractmethod
62+
def save(self, project: str, note: MemoryNote) -> None:
63+
"""Persist ``note`` under ``project``."""
64+
65+
@abstractmethod
66+
def load(self, project: str, note_id: str) -> MemoryNote | None:
67+
"""Retrieve a note by ``note_id`` or ``None`` if it does not exist."""
68+
69+
@abstractmethod
70+
def list(self, project: str, page: int, page_size: int) -> list[MemoryNoteSummary]:
71+
"""Return a paginated list of notes for ``project``."""
72+
73+
@abstractmethod
74+
def delete(self, project: str, note_id: str) -> bool:
75+
"""Remove the note if present and return ``True`` on success."""
76+
77+
78+
class FileMemoryStore(MemoryStore):
79+
"""Filesystem implementation of :class:`MemoryStore`.
80+
81+
Each project is a folder below ``root`` and every note is stored as a
82+
``<id>.md`` file containing YAML front matter for the title and tags
83+
followed by the note content.
84+
"""
85+
86+
def __init__(self, root: Path) -> None:
87+
self.root = root
88+
self.root.mkdir(parents=True, exist_ok=True)
89+
90+
def _project_dir(self, project: str) -> Path:
91+
path = self.root / project
92+
path.mkdir(parents=True, exist_ok=True)
93+
return path
94+
95+
def new_id(self) -> str: # pragma: no cover - simple wrapper
96+
return uuid4().hex
97+
98+
def save(self, project: str, note: MemoryNote) -> None:
99+
path = self._project_dir(project) / f"{note.id}.md"
100+
frontmatter = yaml.safe_dump({"title": note.title, "tags": note.tags}, sort_keys=False)
101+
with path.open("w", encoding="utf-8") as f:
102+
f.write("---\n")
103+
f.write(frontmatter)
104+
f.write("---\n")
105+
f.write(note.content)
106+
107+
def load(self, project: str, note_id: str) -> MemoryNote | None:
108+
path = self._project_dir(project) / f"{note_id}.md"
109+
if not path.exists():
110+
return None
111+
text = path.read_text(encoding="utf-8")
112+
if not text.startswith("---"):
113+
return None
114+
_, rest = text.split("---", 1)
115+
fm, content = rest.split("---", 1)
116+
meta = yaml.safe_load(fm) or {}
117+
return MemoryNote(
118+
id=note_id,
119+
title=meta.get("title", ""),
120+
tags=meta.get("tags", []),
121+
content=content.lstrip(),
122+
)
123+
124+
def list(self, project: str, page: int, page_size: int) -> list[MemoryNoteSummary]:
125+
p = self._project_dir(project)
126+
files = sorted(p.glob("*.md"))
127+
start = (page - 1) * page_size
128+
end = start + page_size
129+
notes: list[MemoryNoteSummary] = []
130+
for file in files[start:end]:
131+
note = self.load(project, file.stem)
132+
if note:
133+
notes.append(MemoryNoteSummary(id=note.id, title=note.title))
134+
return notes
135+
136+
def delete(self, project: str, note_id: str) -> bool:
137+
path = self._project_dir(project) / f"{note_id}.md"
138+
if path.exists():
139+
path.unlink()
140+
return True
141+
return False
142+
143+
144+
class MemoryProject:
145+
"""Groups notes under a project name and delegates storage actions."""
146+
147+
def __init__(self, name: str, store: MemoryStore) -> None:
148+
self.name = name
149+
self.store = store
150+
151+
def create_note(self, title: str, content: str, tags: list[str] | None = None) -> MemoryNote:
152+
note = MemoryNote(id=self.store.new_id(), title=title, content=content, tags=tags or [])
153+
self.store.save(self.name, note)
154+
return note
155+
156+
def get_note(self, note_id: str) -> MemoryNote | None:
157+
return self.store.load(self.name, note_id)
158+
159+
def list_notes(self, page: int = 1, page_size: int = 10) -> list[MemoryNoteSummary]:
160+
return self.store.list(self.name, page, page_size)
161+
162+
def update_note(self, note_id: str, patch: dict[str, object]) -> MemoryNote:
163+
note = self.get_note(note_id)
164+
if note is None:
165+
raise KeyError(note_id)
166+
updated = note.model_copy(update=patch)
167+
self.store.save(self.name, updated)
168+
return updated
169+
170+
def delete_note(self, note_id: str) -> bool:
171+
return self.store.delete(self.name, note_id)

tests/test_examples.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"shop_api_sqlite/app.py",
1212
"sqlalchemy_shop/app.py",
1313
"shop_api_gateway/app.py",
14+
"basic_memory/app.py",
1415
"mutable_crud/app.py",
1516
]
1617

0 commit comments

Comments
 (0)