fix: Ensure datetime serialization includes timezone info for RFC 3339 compliance

Add field_serializer methods to all datetime fields in memory schemas to ensure
proper timezone-aware datetime serialization. This fixes validation errors in
MCP tools like build_context where datetime strings were missing timezone
information.

- Added ensure_timezone_aware utility function
- Updated EntitySummary, RelationSummary, ObservationSummary, MemoryMetadata
- All datetime fields now serialize with timezone suffix (Z or +00:00)
- Handles both timezone-aware and naive datetimes by converting to UTC

Fixes #253

Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
This commit is contained in:
claude[bot]
2025-08-22 15:41:10 +00:00
parent f3d8d8d617
commit 8d954870a5
2 changed files with 122 additions and 2 deletions
+29 -2
View File
@@ -1,14 +1,21 @@
"""Schemas for memory context."""
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional, Annotated, Sequence, Literal, Union
from annotated_types import MinLen, MaxLen
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter, field_serializer
from basic_memory.schemas.search import SearchItemType
def ensure_timezone_aware(dt: datetime) -> datetime:
"""Ensure datetime is timezone-aware by converting to UTC if naive."""
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def validate_memory_url_path(path: str) -> bool:
"""Validate that a memory URL path is well-formed.
@@ -125,6 +132,11 @@ class EntitySummary(BaseModel):
file_path: str
created_at: datetime
@field_serializer('created_at')
def serialize_created_at(self, dt: datetime) -> str:
"""Serialize datetime as RFC 3339 compliant string with timezone."""
return ensure_timezone_aware(dt).isoformat()
class RelationSummary(BaseModel):
"""Simplified relation representation."""
@@ -138,6 +150,11 @@ class RelationSummary(BaseModel):
to_entity: Optional[str] = None
created_at: datetime
@field_serializer('created_at')
def serialize_created_at(self, dt: datetime) -> str:
"""Serialize datetime as RFC 3339 compliant string with timezone."""
return ensure_timezone_aware(dt).isoformat()
class ObservationSummary(BaseModel):
"""Simplified observation representation."""
@@ -150,6 +167,11 @@ class ObservationSummary(BaseModel):
content: str
created_at: datetime
@field_serializer('created_at')
def serialize_created_at(self, dt: datetime) -> str:
"""Serialize datetime as RFC 3339 compliant string with timezone."""
return ensure_timezone_aware(dt).isoformat()
class MemoryMetadata(BaseModel):
"""Simplified response metadata."""
@@ -165,6 +187,11 @@ class MemoryMetadata(BaseModel):
total_relations: Optional[int] = None
total_observations: Optional[int] = None
@field_serializer('generated_at')
def serialize_generated_at(self, dt: datetime) -> str:
"""Serialize datetime as RFC 3339 compliant string with timezone."""
return ensure_timezone_aware(dt).isoformat()
class ContextResult(BaseModel):
"""Context result containing a primary item with its observations and related items."""
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Test script to verify datetime serialization fix."""
import json
from datetime import datetime, timezone
from basic_memory.schemas.memory import EntitySummary, RelationSummary, ObservationSummary, MemoryMetadata
def test_datetime_serialization():
"""Test that datetime fields are serialized with timezone information."""
# Test EntitySummary
entity = EntitySummary(
permalink="/test",
title="Test Entity",
file_path="/test.md",
created_at=datetime.now(timezone.utc)
)
entity_json = entity.model_dump_json()
entity_dict = json.loads(entity_json)
print("EntitySummary created_at:", entity_dict["created_at"])
assert entity_dict["created_at"].endswith("+00:00") or entity_dict["created_at"].endswith("Z"), \
f"EntitySummary created_at should have timezone: {entity_dict['created_at']}"
# Test RelationSummary
relation = RelationSummary(
title="Test Relation",
file_path="/test.md",
permalink="/test-rel",
relation_type="relates_to",
created_at=datetime.now(timezone.utc)
)
relation_json = relation.model_dump_json()
relation_dict = json.loads(relation_json)
print("RelationSummary created_at:", relation_dict["created_at"])
assert relation_dict["created_at"].endswith("+00:00") or relation_dict["created_at"].endswith("Z"), \
f"RelationSummary created_at should have timezone: {relation_dict['created_at']}"
# Test ObservationSummary
observation = ObservationSummary(
title="Test Observation",
file_path="/test.md",
permalink="/test-obs",
category="test",
content="Test content",
created_at=datetime.now(timezone.utc)
)
observation_json = observation.model_dump_json()
observation_dict = json.loads(observation_json)
print("ObservationSummary created_at:", observation_dict["created_at"])
assert observation_dict["created_at"].endswith("+00:00") or observation_dict["created_at"].endswith("Z"), \
f"ObservationSummary created_at should have timezone: {observation_dict['created_at']}"
# Test MemoryMetadata
metadata = MemoryMetadata(
depth=1,
generated_at=datetime.now(timezone.utc)
)
metadata_json = metadata.model_dump_json()
metadata_dict = json.loads(metadata_json)
print("MemoryMetadata generated_at:", metadata_dict["generated_at"])
assert metadata_dict["generated_at"].endswith("+00:00") or metadata_dict["generated_at"].endswith("Z"), \
f"MemoryMetadata generated_at should have timezone: {metadata_dict['generated_at']}"
print("✅ All datetime serialization tests passed!")
# Test with naive datetime to ensure it gets converted to UTC
naive_dt = datetime(2025, 8, 22, 15, 30, 45, 123456)
entity_naive = EntitySummary(
permalink="/test-naive",
title="Test Naive Entity",
file_path="/test-naive.md",
created_at=naive_dt
)
entity_naive_json = entity_naive.model_dump_json()
entity_naive_dict = json.loads(entity_naive_json)
print("Naive datetime converted to:", entity_naive_dict["created_at"])
assert entity_naive_dict["created_at"].endswith("+00:00") or entity_naive_dict["created_at"].endswith("Z"), \
f"Naive datetime should be converted to UTC: {entity_naive_dict['created_at']}"
print("✅ Naive datetime conversion test passed!")
if __name__ == "__main__":
test_datetime_serialization()