basic-memory 0.7.0__py3-none-any.whl → 0.17.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,427 @@
1
+ """V2 Knowledge Router - External ID-based entity operations.
2
+
3
+ This router provides external_id (UUID) based CRUD operations for entities,
4
+ using stable string UUIDs that won't change with file moves or database migrations.
5
+
6
+ Key improvements:
7
+ - Stable external UUIDs that won't change with file moves or renames
8
+ - Better API ergonomics with consistent string identifiers
9
+ - Direct database lookups via unique indexed column
10
+ - Simplified caching strategies
11
+ """
12
+
13
+ from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Response, Path
14
+ from loguru import logger
15
+
16
+ from basic_memory.deps import (
17
+ EntityServiceV2ExternalDep,
18
+ SearchServiceV2ExternalDep,
19
+ LinkResolverV2ExternalDep,
20
+ ProjectConfigV2ExternalDep,
21
+ AppConfigDep,
22
+ SyncServiceV2ExternalDep,
23
+ EntityRepositoryV2ExternalDep,
24
+ ProjectExternalIdPathDep,
25
+ )
26
+ from basic_memory.schemas import DeleteEntitiesResponse
27
+ from basic_memory.schemas.base import Entity
28
+ from basic_memory.schemas.request import EditEntityRequest
29
+ from basic_memory.schemas.v2 import (
30
+ EntityResolveRequest,
31
+ EntityResolveResponse,
32
+ EntityResponseV2,
33
+ MoveEntityRequestV2,
34
+ )
35
+
36
+ router = APIRouter(prefix="/knowledge", tags=["knowledge-v2"])
37
+
38
+
39
+ async def resolve_relations_background(sync_service, entity_id: int, entity_permalink: str) -> None:
40
+ """Background task to resolve relations for a specific entity.
41
+
42
+ This runs asynchronously after the API response is sent, preventing
43
+ long delays when creating entities with many relations.
44
+ """
45
+ try: # pragma: no cover
46
+ # Only resolve relations for the newly created entity
47
+ await sync_service.resolve_relations(entity_id=entity_id) # pragma: no cover
48
+ logger.debug( # pragma: no cover
49
+ f"Background: Resolved relations for entity {entity_permalink} (id={entity_id})"
50
+ )
51
+ except Exception as e: # pragma: no cover
52
+ # Log but don't fail - this is a background task
53
+ logger.warning( # pragma: no cover
54
+ f"Background: Failed to resolve relations for entity {entity_permalink}: {e}"
55
+ )
56
+
57
+
58
+ ## Resolution endpoint
59
+
60
+
61
+ @router.post("/resolve", response_model=EntityResolveResponse)
62
+ async def resolve_identifier(
63
+ project_id: ProjectExternalIdPathDep,
64
+ data: EntityResolveRequest,
65
+ link_resolver: LinkResolverV2ExternalDep,
66
+ entity_repository: EntityRepositoryV2ExternalDep,
67
+ ) -> EntityResolveResponse:
68
+ """Resolve a string identifier (external_id, permalink, title, or path) to entity info.
69
+
70
+ This endpoint provides a bridge between v1-style identifiers and v2 external_ids.
71
+ Use this to convert existing references to the new UUID-based format.
72
+
73
+ Args:
74
+ data: Request containing the identifier to resolve
75
+
76
+ Returns:
77
+ Entity external_id and metadata about how it was resolved
78
+
79
+ Raises:
80
+ HTTPException: 404 if identifier cannot be resolved
81
+
82
+ Example:
83
+ POST /v2/{project_id}/knowledge/resolve
84
+ {"identifier": "specs/search"}
85
+
86
+ Returns:
87
+ {
88
+ "external_id": "550e8400-e29b-41d4-a716-446655440000",
89
+ "entity_id": 123,
90
+ "permalink": "specs/search",
91
+ "file_path": "specs/search.md",
92
+ "title": "Search Specification",
93
+ "resolution_method": "permalink"
94
+ }
95
+ """
96
+ logger.info(f"API v2 request: resolve_identifier for '{data.identifier}'")
97
+
98
+ # Try to resolve by external_id first
99
+ entity = await entity_repository.get_by_external_id(data.identifier)
100
+ resolution_method = "external_id" if entity else "search"
101
+
102
+ # If not found by external_id, try other resolution methods
103
+ if not entity:
104
+ entity = await link_resolver.resolve_link(data.identifier)
105
+ if entity:
106
+ # Determine resolution method
107
+ if entity.permalink == data.identifier:
108
+ resolution_method = "permalink"
109
+ elif entity.title == data.identifier:
110
+ resolution_method = "title"
111
+ elif entity.file_path == data.identifier:
112
+ resolution_method = "path"
113
+ else:
114
+ resolution_method = "search"
115
+
116
+ if not entity:
117
+ raise HTTPException(status_code=404, detail=f"Entity not found: '{data.identifier}'")
118
+
119
+ result = EntityResolveResponse(
120
+ external_id=entity.external_id,
121
+ entity_id=entity.id,
122
+ permalink=entity.permalink,
123
+ file_path=entity.file_path,
124
+ title=entity.title,
125
+ resolution_method=resolution_method,
126
+ )
127
+
128
+ logger.info(
129
+ f"API v2 response: resolved '{data.identifier}' to external_id={result.external_id} via {resolution_method}"
130
+ )
131
+
132
+ return result
133
+
134
+
135
+ ## Read endpoints
136
+
137
+
138
+ @router.get("/entities/{entity_id}", response_model=EntityResponseV2)
139
+ async def get_entity_by_id(
140
+ project_id: ProjectExternalIdPathDep,
141
+ entity_repository: EntityRepositoryV2ExternalDep,
142
+ entity_id: str = Path(..., description="Entity external ID (UUID)"),
143
+ ) -> EntityResponseV2:
144
+ """Get an entity by its external ID (UUID).
145
+
146
+ This is the primary entity retrieval method in v2, using stable UUID
147
+ identifiers that won't change with file moves.
148
+
149
+ Args:
150
+ entity_id: External ID (UUID string)
151
+
152
+ Returns:
153
+ Complete entity with observations and relations
154
+
155
+ Raises:
156
+ HTTPException: 404 if entity not found
157
+ """
158
+ logger.info(f"API v2 request: get_entity_by_id entity_id={entity_id}")
159
+
160
+ entity = await entity_repository.get_by_external_id(entity_id)
161
+ if not entity:
162
+ raise HTTPException(
163
+ status_code=404, detail=f"Entity with external_id '{entity_id}' not found"
164
+ )
165
+
166
+ result = EntityResponseV2.model_validate(entity)
167
+ logger.info(f"API v2 response: external_id={entity_id}, title='{result.title}'")
168
+
169
+ return result
170
+
171
+
172
+ ## Create endpoints
173
+
174
+
175
+ @router.post("/entities", response_model=EntityResponseV2)
176
+ async def create_entity(
177
+ project_id: ProjectExternalIdPathDep,
178
+ data: Entity,
179
+ background_tasks: BackgroundTasks,
180
+ entity_service: EntityServiceV2ExternalDep,
181
+ search_service: SearchServiceV2ExternalDep,
182
+ ) -> EntityResponseV2:
183
+ """Create a new entity.
184
+
185
+ Args:
186
+ data: Entity data to create
187
+
188
+ Returns:
189
+ Created entity with generated external_id (UUID)
190
+ """
191
+ logger.info(
192
+ "API v2 request", endpoint="create_entity", entity_type=data.entity_type, title=data.title
193
+ )
194
+
195
+ entity = await entity_service.create_entity(data)
196
+
197
+ # reindex
198
+ await search_service.index_entity(entity, background_tasks=background_tasks)
199
+ result = EntityResponseV2.model_validate(entity)
200
+
201
+ logger.info(
202
+ f"API v2 response: endpoint='create_entity' external_id={entity.external_id}, title={result.title}, permalink={result.permalink}, status_code=201"
203
+ )
204
+ return result
205
+
206
+
207
+ ## Update endpoints
208
+
209
+
210
+ @router.put("/entities/{entity_id}", response_model=EntityResponseV2)
211
+ async def update_entity_by_id(
212
+ data: Entity,
213
+ response: Response,
214
+ background_tasks: BackgroundTasks,
215
+ project_id: ProjectExternalIdPathDep,
216
+ entity_service: EntityServiceV2ExternalDep,
217
+ search_service: SearchServiceV2ExternalDep,
218
+ sync_service: SyncServiceV2ExternalDep,
219
+ entity_repository: EntityRepositoryV2ExternalDep,
220
+ entity_id: str = Path(..., description="Entity external ID (UUID)"),
221
+ ) -> EntityResponseV2:
222
+ """Update an entity by external ID.
223
+
224
+ If the entity doesn't exist, it will be created (upsert behavior).
225
+
226
+ Args:
227
+ entity_id: External ID (UUID string)
228
+ data: Updated entity data
229
+
230
+ Returns:
231
+ Updated entity
232
+ """
233
+ logger.info(f"API v2 request: update_entity_by_id entity_id={entity_id}")
234
+
235
+ # Check if entity exists
236
+ existing = await entity_repository.get_by_external_id(entity_id)
237
+ created = existing is None
238
+
239
+ # Perform update or create
240
+ entity, _ = await entity_service.create_or_update_entity(data)
241
+ response.status_code = 201 if created else 200
242
+
243
+ # reindex
244
+ await search_service.index_entity(entity, background_tasks=background_tasks)
245
+
246
+ # Schedule relation resolution for new entities
247
+ if created:
248
+ background_tasks.add_task( # pragma: no cover
249
+ resolve_relations_background, sync_service, entity.id, entity.permalink or ""
250
+ )
251
+
252
+ result = EntityResponseV2.model_validate(entity)
253
+
254
+ logger.info(
255
+ f"API v2 response: external_id={entity_id}, created={created}, status_code={response.status_code}"
256
+ )
257
+ return result
258
+
259
+
260
+ @router.patch("/entities/{entity_id}", response_model=EntityResponseV2)
261
+ async def edit_entity_by_id(
262
+ data: EditEntityRequest,
263
+ background_tasks: BackgroundTasks,
264
+ project_id: ProjectExternalIdPathDep,
265
+ entity_service: EntityServiceV2ExternalDep,
266
+ search_service: SearchServiceV2ExternalDep,
267
+ entity_repository: EntityRepositoryV2ExternalDep,
268
+ entity_id: str = Path(..., description="Entity external ID (UUID)"),
269
+ ) -> EntityResponseV2:
270
+ """Edit an existing entity by external ID using operations like append, prepend, etc.
271
+
272
+ Args:
273
+ entity_id: External ID (UUID string)
274
+ data: Edit operation details
275
+
276
+ Returns:
277
+ Updated entity
278
+
279
+ Raises:
280
+ HTTPException: 404 if entity not found, 400 if edit fails
281
+ """
282
+ logger.info(
283
+ f"API v2 request: edit_entity_by_id entity_id={entity_id}, operation='{data.operation}'"
284
+ )
285
+
286
+ # Verify entity exists
287
+ entity = await entity_repository.get_by_external_id(entity_id)
288
+ if not entity: # pragma: no cover
289
+ raise HTTPException(
290
+ status_code=404, detail=f"Entity with external_id '{entity_id}' not found"
291
+ )
292
+
293
+ try:
294
+ # Edit using the entity's permalink or path
295
+ identifier = entity.permalink or entity.file_path
296
+ updated_entity = await entity_service.edit_entity(
297
+ identifier=identifier,
298
+ operation=data.operation,
299
+ content=data.content,
300
+ section=data.section,
301
+ find_text=data.find_text,
302
+ expected_replacements=data.expected_replacements,
303
+ )
304
+
305
+ # Reindex
306
+ await search_service.index_entity(updated_entity, background_tasks=background_tasks)
307
+
308
+ result = EntityResponseV2.model_validate(updated_entity)
309
+
310
+ logger.info(
311
+ f"API v2 response: external_id={entity_id}, operation='{data.operation}', status_code=200"
312
+ )
313
+
314
+ return result
315
+
316
+ except Exception as e:
317
+ logger.error(f"Error editing entity {entity_id}: {e}")
318
+ raise HTTPException(status_code=400, detail=str(e))
319
+
320
+
321
+ ## Delete endpoints
322
+
323
+
324
+ @router.delete("/entities/{entity_id}", response_model=DeleteEntitiesResponse)
325
+ async def delete_entity_by_id(
326
+ background_tasks: BackgroundTasks,
327
+ project_id: ProjectExternalIdPathDep,
328
+ entity_service: EntityServiceV2ExternalDep,
329
+ entity_repository: EntityRepositoryV2ExternalDep,
330
+ entity_id: str = Path(..., description="Entity external ID (UUID)"),
331
+ search_service=Depends(lambda: None), # Optional for now
332
+ ) -> DeleteEntitiesResponse:
333
+ """Delete an entity by external ID.
334
+
335
+ Args:
336
+ entity_id: External ID (UUID string)
337
+
338
+ Returns:
339
+ Deletion status
340
+
341
+ Note: Returns deleted=False if entity doesn't exist (idempotent)
342
+ """
343
+ logger.info(f"API v2 request: delete_entity_by_id entity_id={entity_id}")
344
+
345
+ entity = await entity_repository.get_by_external_id(entity_id)
346
+ if entity is None:
347
+ logger.info(f"API v2 response: external_id={entity_id} not found, deleted=False")
348
+ return DeleteEntitiesResponse(deleted=False)
349
+
350
+ # Delete the entity using internal ID
351
+ deleted = await entity_service.delete_entity(entity.id)
352
+
353
+ # Remove from search index if search service available
354
+ if search_service:
355
+ background_tasks.add_task(search_service.handle_delete, entity) # pragma: no cover
356
+
357
+ logger.info(f"API v2 response: external_id={entity_id}, deleted={deleted}")
358
+
359
+ return DeleteEntitiesResponse(deleted=deleted)
360
+
361
+
362
+ ## Move endpoint
363
+
364
+
365
+ @router.put("/entities/{entity_id}/move", response_model=EntityResponseV2)
366
+ async def move_entity(
367
+ data: MoveEntityRequestV2,
368
+ background_tasks: BackgroundTasks,
369
+ project_id: ProjectExternalIdPathDep,
370
+ entity_service: EntityServiceV2ExternalDep,
371
+ entity_repository: EntityRepositoryV2ExternalDep,
372
+ project_config: ProjectConfigV2ExternalDep,
373
+ app_config: AppConfigDep,
374
+ search_service: SearchServiceV2ExternalDep,
375
+ entity_id: str = Path(..., description="Entity external ID (UUID)"),
376
+ ) -> EntityResponseV2:
377
+ """Move an entity to a new file location.
378
+
379
+ V2 API uses external_id (UUID) in the URL path for stable references.
380
+ The external_id will remain stable after the move.
381
+
382
+ Args:
383
+ project_id: Project external ID from URL path
384
+ entity_id: Entity external ID from URL path (primary identifier)
385
+ data: Move request with destination path only
386
+
387
+ Returns:
388
+ Updated entity with new file path
389
+ """
390
+ logger.info(
391
+ f"API v2 request: move_entity entity_id={entity_id}, destination='{data.destination_path}'"
392
+ )
393
+
394
+ try:
395
+ # First, get the entity by external_id to verify it exists
396
+ entity = await entity_repository.get_by_external_id(entity_id)
397
+ if not entity: # pragma: no cover
398
+ raise HTTPException(
399
+ status_code=404, detail=f"Entity with external_id '{entity_id}' not found"
400
+ )
401
+
402
+ # Move the entity using its current file path as identifier
403
+ moved_entity = await entity_service.move_entity(
404
+ identifier=entity.file_path, # Use file path for resolution
405
+ destination_path=data.destination_path,
406
+ project_config=project_config,
407
+ app_config=app_config,
408
+ )
409
+
410
+ # Reindex at new location
411
+ reindexed_entity = await entity_service.link_resolver.resolve_link(data.destination_path)
412
+ if reindexed_entity:
413
+ await search_service.index_entity(reindexed_entity, background_tasks=background_tasks)
414
+
415
+ result = EntityResponseV2.model_validate(moved_entity)
416
+
417
+ logger.info(
418
+ f"API v2 response: moved external_id={entity_id} to '{data.destination_path}'"
419
+ )
420
+
421
+ return result
422
+
423
+ except HTTPException: # pragma: no cover
424
+ raise # pragma: no cover
425
+ except Exception as e:
426
+ logger.error(f"Error moving entity: {e}")
427
+ raise HTTPException(status_code=400, detail=str(e))
@@ -0,0 +1,130 @@
1
+ """V2 routes for memory:// URI operations.
2
+
3
+ This router uses external_id UUIDs for stable, API-friendly routing.
4
+ V1 uses string-based project names which are less efficient and less stable.
5
+ """
6
+
7
+ from typing import Annotated, Optional
8
+
9
+ from fastapi import APIRouter, Query, Path
10
+ from loguru import logger
11
+
12
+ from basic_memory.deps import ContextServiceV2ExternalDep, EntityRepositoryV2ExternalDep
13
+ from basic_memory.schemas.base import TimeFrame, parse_timeframe
14
+ from basic_memory.schemas.memory import (
15
+ GraphContext,
16
+ normalize_memory_url,
17
+ )
18
+ from basic_memory.schemas.search import SearchItemType
19
+ from basic_memory.api.routers.utils import to_graph_context
20
+
21
+ # Note: No prefix here - it's added during registration as /v2/{project_id}/memory
22
+ router = APIRouter(tags=["memory"])
23
+
24
+
25
+ @router.get("/memory/recent", response_model=GraphContext)
26
+ async def recent(
27
+ context_service: ContextServiceV2ExternalDep,
28
+ entity_repository: EntityRepositoryV2ExternalDep,
29
+ project_id: str = Path(..., description="Project external UUID"),
30
+ type: Annotated[list[SearchItemType] | None, Query()] = None,
31
+ depth: int = 1,
32
+ timeframe: TimeFrame = "7d",
33
+ page: int = 1,
34
+ page_size: int = 10,
35
+ max_related: int = 10,
36
+ ) -> GraphContext:
37
+ """Get recent activity context for a project.
38
+
39
+ Args:
40
+ project_id: Project external UUID from URL path
41
+ context_service: Context service scoped to project
42
+ entity_repository: Entity repository scoped to project
43
+ type: Types of items to include (entities, relations, observations)
44
+ depth: How many levels of related entities to include
45
+ timeframe: Time window for recent activity (e.g., "7d", "1 week")
46
+ page: Page number for pagination
47
+ page_size: Number of items per page
48
+ max_related: Maximum related entities to include per item
49
+
50
+ Returns:
51
+ GraphContext with recent activity and related entities
52
+ """
53
+ # return all types by default
54
+ types = (
55
+ [SearchItemType.ENTITY, SearchItemType.RELATION, SearchItemType.OBSERVATION]
56
+ if not type
57
+ else type
58
+ )
59
+
60
+ logger.debug(
61
+ f"V2 Getting recent context for project {project_id}: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
62
+ )
63
+ # Parse timeframe
64
+ since = parse_timeframe(timeframe)
65
+ limit = page_size
66
+ offset = (page - 1) * page_size
67
+
68
+ # Build context
69
+ context = await context_service.build_context(
70
+ types=types, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
71
+ )
72
+ recent_context = await to_graph_context(
73
+ context, entity_repository=entity_repository, page=page, page_size=page_size
74
+ )
75
+ logger.debug(f"V2 Recent context: {recent_context.model_dump_json()}")
76
+ return recent_context
77
+
78
+
79
+ # get_memory_context needs to be declared last so other paths can match
80
+
81
+
82
+ @router.get("/memory/{uri:path}", response_model=GraphContext)
83
+ async def get_memory_context(
84
+ context_service: ContextServiceV2ExternalDep,
85
+ entity_repository: EntityRepositoryV2ExternalDep,
86
+ uri: str,
87
+ project_id: str = Path(..., description="Project external UUID"),
88
+ depth: int = 1,
89
+ timeframe: Optional[TimeFrame] = None,
90
+ page: int = 1,
91
+ page_size: int = 10,
92
+ max_related: int = 10,
93
+ ) -> GraphContext:
94
+ """Get rich context from memory:// URI.
95
+
96
+ V2 supports both legacy path-based URIs and new ID-based URIs:
97
+ - Legacy: memory://path/to/note
98
+ - ID-based: memory://id/123 or memory://123
99
+
100
+ Args:
101
+ project_id: Project external UUID from URL path
102
+ context_service: Context service scoped to project
103
+ entity_repository: Entity repository scoped to project
104
+ uri: Memory URI path (e.g., "id/123", "123", or "path/to/note")
105
+ depth: How many levels of related entities to include
106
+ timeframe: Optional time window for filtering related content
107
+ page: Page number for pagination
108
+ page_size: Number of items per page
109
+ max_related: Maximum related entities to include
110
+
111
+ Returns:
112
+ GraphContext with the entity and its related context
113
+ """
114
+ logger.debug(
115
+ f"V2 Getting context for project {project_id}, URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
116
+ )
117
+ memory_url = normalize_memory_url(uri)
118
+
119
+ # Parse timeframe
120
+ since = parse_timeframe(timeframe) if timeframe else None
121
+ limit = page_size
122
+ offset = (page - 1) * page_size
123
+
124
+ # Build context
125
+ context = await context_service.build_context(
126
+ memory_url, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
127
+ )
128
+ return await to_graph_context(
129
+ context, entity_repository=entity_repository, page=page, page_size=page_size
130
+ )