Skip to content

Commit 231c795

Browse files
jope-bmclaude
andcommitted
feat: Add database IDs to all V2 API entity responses
Ensure all V2 API endpoints return database IDs for entities, observations, and relations to enable consistent ID-based references across the API. Schema changes: - Add entity_id, observation_id, relation_id fields to SearchResult - Add entity_id to EntitySummary - Add relation_id, entity_id, from_entity_id, to_entity_id to RelationSummary - Add observation_id, entity_id to ObservationSummary Implementation changes: - Update to_summary() to populate all entity/observation/relation IDs - Update to_search_results() to set appropriate IDs based on type Test updates: - Update test fixtures to include new required ID fields - All 180 V2 API tests passing - All schema serialization tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joe P <joe@basicmemory.com>
1 parent 887da3f commit 231c795

6 files changed

Lines changed: 61 additions & 1 deletion

File tree

src/basic_memory/api/routers/utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ async def to_summary(item: SearchIndexRow | ContextResultRow):
2929
match item.type:
3030
case SearchItemType.ENTITY:
3131
return EntitySummary(
32+
entity_id=item.id,
3233
title=item.title, # pyright: ignore
3334
permalink=item.permalink,
3435
content=item.content,
@@ -37,6 +38,8 @@ async def to_summary(item: SearchIndexRow | ContextResultRow):
3738
)
3839
case SearchItemType.OBSERVATION:
3940
return ObservationSummary(
41+
observation_id=item.id,
42+
entity_id=item.entity_id, # pyright: ignore
4043
title=item.title, # pyright: ignore
4144
file_path=item.file_path,
4245
category=item.category, # pyright: ignore
@@ -48,12 +51,16 @@ async def to_summary(item: SearchIndexRow | ContextResultRow):
4851
from_entity = await entity_repository.find_by_id(item.from_id) # pyright: ignore
4952
to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
5053
return RelationSummary(
54+
relation_id=item.id,
55+
entity_id=item.entity_id, # pyright: ignore
5156
title=item.title, # pyright: ignore
5257
file_path=item.file_path,
5358
permalink=item.permalink, # pyright: ignore
5459
relation_type=item.relation_type, # pyright: ignore
5560
from_entity=from_entity.title if from_entity else None,
61+
from_entity_id=item.from_id, # pyright: ignore
5662
to_entity=to_entity.title if to_entity else None,
63+
to_entity_id=item.to_id,
5764
created_at=item.created_at,
5865
)
5966
case _: # pragma: no cover
@@ -111,6 +118,21 @@ async def to_search_results(entity_service: EntityService, results: List[SearchI
111118
search_results = []
112119
for r in results:
113120
entities = await entity_service.get_entities_by_id([r.entity_id, r.from_id, r.to_id]) # pyright: ignore
121+
122+
# Determine which IDs to set based on type
123+
entity_id = None
124+
observation_id = None
125+
relation_id = None
126+
127+
if r.type == SearchItemType.ENTITY:
128+
entity_id = r.id
129+
elif r.type == SearchItemType.OBSERVATION:
130+
observation_id = r.id
131+
entity_id = r.entity_id # Parent entity
132+
elif r.type == SearchItemType.RELATION:
133+
relation_id = r.id
134+
entity_id = r.entity_id # Parent entity
135+
114136
search_results.append(
115137
SearchResult(
116138
title=r.title, # pyright: ignore
@@ -121,6 +143,9 @@ async def to_search_results(entity_service: EntityService, results: List[SearchI
121143
content=r.content,
122144
file_path=r.file_path,
123145
metadata=r.metadata,
146+
entity_id=entity_id,
147+
observation_id=observation_id,
148+
relation_id=relation_id,
124149
category=r.category,
125150
from_entity=entities[0].permalink if entities else None,
126151
to_entity=entities[1].permalink if len(entities) > 1 else None,

src/basic_memory/schemas/memory.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class EntitySummary(BaseModel):
124124
"""Simplified entity representation."""
125125

126126
type: Literal["entity"] = "entity"
127+
entity_id: int # Database ID for v2 API consistency
127128
permalink: Optional[str]
128129
title: str
129130
content: Optional[str] = None
@@ -141,12 +142,16 @@ class RelationSummary(BaseModel):
141142
"""Simplified relation representation."""
142143

143144
type: Literal["relation"] = "relation"
145+
relation_id: int # Database ID for v2 API consistency
146+
entity_id: Optional[int] = None # ID of the entity this relation belongs to
144147
title: str
145148
file_path: str
146149
permalink: str
147150
relation_type: str
148151
from_entity: Optional[str] = None
152+
from_entity_id: Optional[int] = None # ID of source entity
149153
to_entity: Optional[str] = None
154+
to_entity_id: Optional[int] = None # ID of target entity
150155
created_at: Annotated[
151156
datetime, Field(json_schema_extra={"type": "string", "format": "date-time"})
152157
]
@@ -160,6 +165,8 @@ class ObservationSummary(BaseModel):
160165
"""Simplified observation representation."""
161166

162167
type: Literal["observation"] = "observation"
168+
observation_id: int # Database ID for v2 API consistency
169+
entity_id: Optional[int] = None # ID of the entity this observation belongs to
163170
title: str
164171
file_path: str
165172
permalink: str

src/basic_memory/schemas/search.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ class SearchResult(BaseModel):
9797

9898
metadata: Optional[dict] = None
9999

100+
# IDs for v2 API consistency
101+
entity_id: Optional[int] = None # Entity ID (always present for entities)
102+
observation_id: Optional[int] = None # Observation ID (for observation results)
103+
relation_id: Optional[int] = None # Relation ID (for relation results)
104+
100105
# Type-specific fields
101106
category: Optional[str] = None # For observations
102107
from_entity: Optional[Permalink] = None # For relations

tests/api/test_continue_conversation_template.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def template_loader():
1818
def entity_summary():
1919
"""Create a sample EntitySummary for testing."""
2020
return EntitySummary(
21+
entity_id=1,
2122
title="Test Entity",
2223
permalink="test/entity",
2324
type=SearchItemType.ENTITY,
@@ -34,6 +35,8 @@ def context_with_results(entity_summary):
3435

3536
# Create an observation for the entity
3637
observation = ObservationSummary(
38+
observation_id=1,
39+
entity_id=1,
3740
title="Test Observation",
3841
permalink="test/entity/observations/1",
3942
category="test",

tests/mcp/test_prompts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def test_prompt_context_with_file_path_no_permalink():
116116

117117
# Create a mock context with a file that has no permalink (like a binary file)
118118
test_entity = EntitySummary(
119+
entity_id=1,
119120
type="entity",
120121
title="Test File",
121122
permalink=None, # No permalink

tests/schemas/test_memory_serialization.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def test_entity_summary_datetime_serialization(self):
2222
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
2323

2424
entity = EntitySummary(
25+
entity_id=1,
2526
permalink="test/entity",
2627
title="Test Entity",
2728
file_path="test/entity.md",
@@ -41,6 +42,8 @@ def test_relation_summary_datetime_serialization(self):
4142
test_datetime = datetime(2023, 12, 8, 15, 45, 30)
4243

4344
relation = RelationSummary(
45+
relation_id=1,
46+
entity_id=1,
4447
title="Test Relation",
4548
file_path="test/relation.md",
4649
permalink="test/relation",
@@ -63,6 +66,8 @@ def test_observation_summary_datetime_serialization(self):
6366
test_datetime = datetime(2023, 12, 8, 20, 15, 45)
6467

6568
observation = ObservationSummary(
69+
observation_id=1,
70+
entity_id=1,
6671
title="Test Observation",
6772
file_path="test/observation.md",
6873
permalink="test/observation",
@@ -100,13 +105,16 @@ def test_context_result_with_datetime_serialization(self):
100105
test_datetime = datetime(2023, 12, 8, 9, 30, 15)
101106

102107
entity = EntitySummary(
108+
entity_id=1,
103109
permalink="test/entity",
104110
title="Test Entity",
105111
file_path="test/entity.md",
106112
created_at=test_datetime,
107113
)
108114

109115
observation = ObservationSummary(
116+
observation_id=1,
117+
entity_id=1,
110118
title="Test Observation",
111119
file_path="test/observation.md",
112120
permalink="test/observation",
@@ -131,6 +139,7 @@ def test_graph_context_full_serialization(self):
131139
test_datetime = datetime(2023, 12, 8, 14, 20, 10)
132140

133141
entity = EntitySummary(
142+
entity_id=1,
134143
permalink="test/entity",
135144
title="Test Entity",
136145
file_path="test/entity.md",
@@ -159,6 +168,7 @@ def test_datetime_with_microseconds_serialization(self):
159168
test_datetime = datetime(2023, 12, 8, 10, 30, 0, 123456)
160169

161170
entity = EntitySummary(
171+
entity_id=1,
162172
permalink="test/entity",
163173
title="Test Entity",
164174
file_path="test/entity.md",
@@ -176,6 +186,7 @@ def test_mcp_schema_validation_compatibility(self):
176186
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
177187

178188
entity = EntitySummary(
189+
entity_id=1,
179190
permalink="test/entity",
180191
title="Test Entity",
181192
file_path="test/entity.md",
@@ -212,10 +223,16 @@ def test_all_models_have_datetime_serializers_configured(self):
212223

213224
if model_class == EntitySummary:
214225
instance = model_class(
215-
permalink="test", title="Test", file_path="test.md", created_at=test_datetime
226+
entity_id=1,
227+
permalink="test",
228+
title="Test",
229+
file_path="test.md",
230+
created_at=test_datetime,
216231
)
217232
elif model_class == RelationSummary:
218233
instance = model_class(
234+
relation_id=1,
235+
entity_id=1,
219236
title="Test",
220237
file_path="test.md",
221238
permalink="test",
@@ -224,6 +241,8 @@ def test_all_models_have_datetime_serializers_configured(self):
224241
)
225242
elif model_class == ObservationSummary:
226243
instance = model_class(
244+
observation_id=1,
245+
entity_id=1,
227246
title="Test",
228247
file_path="test.md",
229248
permalink="test",

0 commit comments

Comments
 (0)