Skip to content

Commit 7aff836

Browse files
jope-bmclaude[bot]claude
authored
fix: Add ISO datetime serialization to MCP schema models (#270)
Signed-off-by: Joe P <joe@basicmemory.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: jope-bm <jope-bm@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 285e96b commit 7aff836

2 files changed

Lines changed: 244 additions & 1 deletion

File tree

src/basic_memory/schemas/memory.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import List, Optional, Annotated, Sequence, Literal, Union
55

66
from annotated_types import MinLen, MaxLen
7-
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter
7+
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter, ConfigDict
88

99
from basic_memory.schemas.search import SearchItemType
1010

@@ -117,6 +117,8 @@ def memory_url_path(url: memory_url) -> str: # pyright: ignore
117117

118118
class EntitySummary(BaseModel):
119119
"""Simplified entity representation."""
120+
121+
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
120122

121123
type: Literal["entity"] = "entity"
122124
permalink: Optional[str]
@@ -128,6 +130,8 @@ class EntitySummary(BaseModel):
128130

129131
class RelationSummary(BaseModel):
130132
"""Simplified relation representation."""
133+
134+
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
131135

132136
type: Literal["relation"] = "relation"
133137
title: str
@@ -141,6 +145,8 @@ class RelationSummary(BaseModel):
141145

142146
class ObservationSummary(BaseModel):
143147
"""Simplified observation representation."""
148+
149+
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
144150

145151
type: Literal["observation"] = "observation"
146152
title: str
@@ -153,6 +159,8 @@ class ObservationSummary(BaseModel):
153159

154160
class MemoryMetadata(BaseModel):
155161
"""Simplified response metadata."""
162+
163+
model_config = ConfigDict(json_encoders={datetime: lambda dt: dt.isoformat()})
156164

157165
uri: Optional[str] = None
158166
types: Optional[List[SearchItemType]] = None
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
"""Tests for datetime serialization in memory schema models."""
2+
3+
import json
4+
from datetime import datetime
5+
6+
import pytest
7+
8+
from basic_memory.schemas.memory import (
9+
EntitySummary,
10+
RelationSummary,
11+
ObservationSummary,
12+
MemoryMetadata,
13+
GraphContext,
14+
ContextResult
15+
)
16+
17+
18+
class TestDateTimeSerialization:
19+
"""Test datetime serialization for MCP schema compliance."""
20+
21+
def test_entity_summary_datetime_serialization(self):
22+
"""Test EntitySummary serializes datetime as ISO format string."""
23+
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
24+
25+
entity = EntitySummary(
26+
permalink="test/entity",
27+
title="Test Entity",
28+
file_path="test/entity.md",
29+
created_at=test_datetime
30+
)
31+
32+
# Test model_dump_json() produces ISO format
33+
json_str = entity.model_dump_json()
34+
data = json.loads(json_str)
35+
36+
assert data["created_at"] == "2023-12-08T10:30:00"
37+
assert data["type"] == "entity"
38+
assert data["title"] == "Test Entity"
39+
40+
def test_relation_summary_datetime_serialization(self):
41+
"""Test RelationSummary serializes datetime as ISO format string."""
42+
test_datetime = datetime(2023, 12, 8, 15, 45, 30)
43+
44+
relation = RelationSummary(
45+
title="Test Relation",
46+
file_path="test/relation.md",
47+
permalink="test/relation",
48+
relation_type="relates_to",
49+
from_entity="entity1",
50+
to_entity="entity2",
51+
created_at=test_datetime
52+
)
53+
54+
# Test model_dump_json() produces ISO format
55+
json_str = relation.model_dump_json()
56+
data = json.loads(json_str)
57+
58+
assert data["created_at"] == "2023-12-08T15:45:30"
59+
assert data["type"] == "relation"
60+
assert data["relation_type"] == "relates_to"
61+
62+
def test_observation_summary_datetime_serialization(self):
63+
"""Test ObservationSummary serializes datetime as ISO format string."""
64+
test_datetime = datetime(2023, 12, 8, 20, 15, 45)
65+
66+
observation = ObservationSummary(
67+
title="Test Observation",
68+
file_path="test/observation.md",
69+
permalink="test/observation",
70+
category="note",
71+
content="Test content",
72+
created_at=test_datetime
73+
)
74+
75+
# Test model_dump_json() produces ISO format
76+
json_str = observation.model_dump_json()
77+
data = json.loads(json_str)
78+
79+
assert data["created_at"] == "2023-12-08T20:15:45"
80+
assert data["type"] == "observation"
81+
assert data["category"] == "note"
82+
83+
def test_memory_metadata_datetime_serialization(self):
84+
"""Test MemoryMetadata serializes datetime as ISO format string."""
85+
test_datetime = datetime(2023, 12, 8, 12, 0, 0)
86+
87+
metadata = MemoryMetadata(
88+
depth=2,
89+
generated_at=test_datetime,
90+
primary_count=5,
91+
related_count=3
92+
)
93+
94+
# Test model_dump_json() produces ISO format
95+
json_str = metadata.model_dump_json()
96+
data = json.loads(json_str)
97+
98+
assert data["generated_at"] == "2023-12-08T12:00:00"
99+
assert data["depth"] == 2
100+
assert data["primary_count"] == 5
101+
102+
def test_context_result_with_datetime_serialization(self):
103+
"""Test ContextResult with nested models serializes datetime correctly."""
104+
test_datetime = datetime(2023, 12, 8, 9, 30, 15)
105+
106+
entity = EntitySummary(
107+
permalink="test/entity",
108+
title="Test Entity",
109+
file_path="test/entity.md",
110+
created_at=test_datetime
111+
)
112+
113+
observation = ObservationSummary(
114+
title="Test Observation",
115+
file_path="test/observation.md",
116+
permalink="test/observation",
117+
category="note",
118+
content="Test content",
119+
created_at=test_datetime
120+
)
121+
122+
context_result = ContextResult(
123+
primary_result=entity,
124+
observations=[observation],
125+
related_results=[]
126+
)
127+
128+
# Test model_dump_json() produces ISO format for nested models
129+
json_str = context_result.model_dump_json()
130+
data = json.loads(json_str)
131+
132+
assert data["primary_result"]["created_at"] == "2023-12-08T09:30:15"
133+
assert data["observations"][0]["created_at"] == "2023-12-08T09:30:15"
134+
135+
def test_graph_context_full_serialization(self):
136+
"""Test full GraphContext serialization with all datetime fields."""
137+
test_datetime = datetime(2023, 12, 8, 14, 20, 10)
138+
139+
entity = EntitySummary(
140+
permalink="test/entity",
141+
title="Test Entity",
142+
file_path="test/entity.md",
143+
created_at=test_datetime
144+
)
145+
146+
metadata = MemoryMetadata(
147+
depth=1,
148+
generated_at=test_datetime,
149+
primary_count=1,
150+
related_count=0
151+
)
152+
153+
context_result = ContextResult(
154+
primary_result=entity,
155+
observations=[],
156+
related_results=[]
157+
)
158+
159+
graph_context = GraphContext(
160+
results=[context_result],
161+
metadata=metadata,
162+
page=1,
163+
page_size=10
164+
)
165+
166+
# Test full serialization
167+
json_str = graph_context.model_dump_json()
168+
data = json.loads(json_str)
169+
170+
assert data["metadata"]["generated_at"] == "2023-12-08T14:20:10"
171+
assert data["results"][0]["primary_result"]["created_at"] == "2023-12-08T14:20:10"
172+
173+
def test_datetime_with_microseconds_serialization(self):
174+
"""Test datetime with microseconds serializes correctly."""
175+
test_datetime = datetime(2023, 12, 8, 10, 30, 0, 123456)
176+
177+
entity = EntitySummary(
178+
permalink="test/entity",
179+
title="Test Entity",
180+
file_path="test/entity.md",
181+
created_at=test_datetime
182+
)
183+
184+
json_str = entity.model_dump_json()
185+
data = json.loads(json_str)
186+
187+
# Should include microseconds in ISO format
188+
assert data["created_at"] == "2023-12-08T10:30:00.123456"
189+
190+
def test_mcp_schema_validation_compatibility(self):
191+
"""Test that serialized datetime format is compatible with MCP schema validation."""
192+
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
193+
194+
entity = EntitySummary(
195+
permalink="test/entity",
196+
title="Test Entity",
197+
file_path="test/entity.md",
198+
created_at=test_datetime
199+
)
200+
201+
# Serialize to JSON
202+
json_str = entity.model_dump_json()
203+
data = json.loads(json_str)
204+
205+
# Verify the format matches expected MCP "date-time" format
206+
datetime_str = data["created_at"]
207+
208+
# Should be parseable back to datetime (ISO format validation)
209+
parsed_datetime = datetime.fromisoformat(datetime_str)
210+
assert parsed_datetime == test_datetime
211+
212+
# Should match the expected ISO format pattern
213+
assert "T" in datetime_str # Contains date-time separator
214+
assert len(datetime_str) >= 19 # At least YYYY-MM-DDTHH:MM:SS format
215+
216+
def test_all_models_have_json_encoders_configured(self):
217+
"""Test that all memory schema models have datetime json_encoders configured."""
218+
models_to_test = [
219+
EntitySummary,
220+
RelationSummary,
221+
ObservationSummary,
222+
MemoryMetadata
223+
]
224+
225+
for model_class in models_to_test:
226+
# Check that ConfigDict with json_encoders is configured
227+
assert hasattr(model_class, 'model_config')
228+
assert 'json_encoders' in model_class.model_config
229+
assert datetime in model_class.model_config['json_encoders']
230+
231+
# Verify the encoder function produces ISO format
232+
encoder = model_class.model_config['json_encoders'][datetime]
233+
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
234+
result = encoder(test_datetime)
235+
assert result == "2023-12-08T10:30:00"

0 commit comments

Comments
 (0)