Skip to content

Commit 373893a

Browse files
phernandezclaude
andcommitted
fix: restore API backward compatibility for v0.18.x clients (#638)
v0.18.5 clients (homebrew) fail against v0.19.0 servers because: - `entity_type` was renamed to `note_type` in EntityResponse - 13 fields gained `Field(exclude=True)` and vanished from JSON 🔧 EntityResponse: add `entity_type` computed_field mirroring `note_type` 🔧 memory.py: remove `exclude=True` from 13 fields across EntitySummary, RelationSummary, ObservationSummary, and MemoryMetadata 🔧 Add v0.18 backward-compat contract tests Closes #638 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 1987581 commit 373893a

3 files changed

Lines changed: 239 additions & 72 deletions

File tree

src/basic_memory/schemas/memory.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ class EntitySummary(BaseModel):
125125

126126
type: Literal["entity"] = "entity"
127127
external_id: str # UUID for v2 API routing
128-
entity_id: Optional[int] = Field(None, exclude=True) # Internal DB ID
128+
# COMPAT(v0.18): old clients expect these fields in JSON
129+
entity_id: Optional[int] = None
129130
permalink: Optional[str]
130131
title: str
131132
content: Optional[str] = None
@@ -143,18 +144,19 @@ class RelationSummary(BaseModel):
143144
"""Simplified relation representation."""
144145

145146
type: Literal["relation"] = "relation"
146-
relation_id: Optional[int] = Field(None, exclude=True) # Internal DB ID
147-
entity_id: Optional[int] = Field(None, exclude=True) # Internal FK
147+
# COMPAT(v0.18): old clients expect these fields in JSON
148+
relation_id: Optional[int] = None
149+
entity_id: Optional[int] = None
148150
title: str
149151
file_path: str
150152
permalink: str
151153
relation_type: str
152154
from_entity: Optional[str] = None
153-
from_entity_id: Optional[int] = Field(None, exclude=True) # Internal FK
154-
from_entity_external_id: Optional[str] = Field(None, exclude=True) # Internal routing ID
155+
from_entity_id: Optional[int] = None
156+
from_entity_external_id: Optional[str] = None
155157
to_entity: Optional[str] = None
156-
to_entity_id: Optional[int] = Field(None, exclude=True) # Internal FK
157-
to_entity_external_id: Optional[str] = Field(None, exclude=True) # Internal routing ID
158+
to_entity_id: Optional[int] = None
159+
to_entity_external_id: Optional[str] = None
158160
created_at: Annotated[
159161
datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
160162
]
@@ -168,10 +170,11 @@ class ObservationSummary(BaseModel):
168170
"""Simplified observation representation."""
169171

170172
type: Literal["observation"] = "observation"
171-
observation_id: Optional[int] = Field(None, exclude=True) # Internal DB ID
172-
entity_id: Optional[int] = Field(None, exclude=True) # Internal FK
173-
entity_external_id: Optional[str] = Field(None, exclude=True) # Internal routing ID
174-
title: Optional[str] = Field(None, exclude=True) # Redundant with parent entity
173+
# COMPAT(v0.18): old clients expect these fields in JSON
174+
observation_id: Optional[int] = None
175+
entity_id: Optional[int] = None
176+
entity_external_id: Optional[str] = None
177+
title: Optional[str] = None
175178
file_path: str
176179
permalink: str
177180
category: str
@@ -192,13 +195,18 @@ class MemoryMetadata(BaseModel):
192195
types: Optional[List[SearchItemType]] = None
193196
depth: int
194197
timeframe: Optional[str] = None
195-
generated_at: Optional[datetime] = Field(None, exclude=True) # Internal timing
196-
primary_count: Optional[int] = None # Changed field name
197-
related_count: Optional[int] = None # Changed field name
198-
total_results: Optional[int] = Field(None, exclude=True) # Internal counter
198+
# COMPAT(v0.18): old clients expect generated_at and total_results in JSON
199+
generated_at: Optional[datetime] = None
200+
primary_count: Optional[int] = None
201+
related_count: Optional[int] = None
202+
total_results: Optional[int] = None
199203
total_relations: Optional[int] = None
200204
total_observations: Optional[int] = None
201205

206+
@field_serializer("generated_at")
207+
def serialize_generated_at(self, dt: Optional[datetime]) -> Optional[str]:
208+
return dt.isoformat() if dt else None
209+
202210

203211
class ContextResult(BaseModel):
204212
"""Context result containing a primary item with its observations and related items."""

src/basic_memory/schemas/response.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from datetime import datetime
1515
from typing import List, Optional, Dict
1616

17-
from pydantic import BaseModel, ConfigDict, Field, model_validator
17+
from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator
1818

1919
from basic_memory.schemas.base import Relation, Permalink, NoteType, ContentType, Observation
2020

@@ -192,6 +192,13 @@ class EntityResponse(SQLAlchemyModel):
192192
title: str
193193
file_path: str
194194
note_type: NoteType
195+
196+
# COMPAT(v0.18): old clients expect entity_type; remove when no longer needed
197+
@computed_field # type: ignore[prop-decorator]
198+
@property
199+
def entity_type(self) -> str:
200+
return self.note_type
201+
195202
entity_metadata: Optional[Dict] = None
196203
checksum: Optional[str] = None
197204
content_type: ContentType

0 commit comments

Comments
 (0)