basic-memory 0.17.1__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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  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 +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  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 +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -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/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,318 @@
1
+ """Router for knowledge graph operations.
2
+
3
+ ⚠️ DEPRECATED: This v1 API is deprecated and will be removed on June 30, 2026.
4
+ Please migrate to /v2/{project}/knowledge endpoints which use entity IDs instead
5
+ of path-based identifiers for improved performance and stability.
6
+
7
+ Migration guide: See docs/migration/v1-to-v2.md
8
+ """
9
+
10
+ from typing import Annotated
11
+
12
+ from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Query, Response
13
+ from loguru import logger
14
+
15
+ from basic_memory.deps import (
16
+ EntityServiceDep,
17
+ get_search_service,
18
+ SearchServiceDep,
19
+ LinkResolverDep,
20
+ ProjectPathDep,
21
+ FileServiceDep,
22
+ ProjectConfigDep,
23
+ AppConfigDep,
24
+ SyncServiceDep,
25
+ )
26
+ from basic_memory.schemas import (
27
+ EntityListResponse,
28
+ EntityResponse,
29
+ DeleteEntitiesResponse,
30
+ DeleteEntitiesRequest,
31
+ )
32
+ from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest
33
+ from basic_memory.schemas.base import Permalink, Entity
34
+
35
+ router = APIRouter(
36
+ prefix="/knowledge",
37
+ tags=["knowledge"],
38
+ deprecated=True, # Marks entire router as deprecated in OpenAPI docs
39
+ )
40
+
41
+
42
+ async def resolve_relations_background(sync_service, entity_id: int, entity_permalink: str) -> None:
43
+ """Background task to resolve relations for a specific entity.
44
+
45
+ This runs asynchronously after the API response is sent, preventing
46
+ long delays when creating entities with many relations.
47
+ """
48
+ try:
49
+ # Only resolve relations for the newly created entity
50
+ await sync_service.resolve_relations(entity_id=entity_id)
51
+ logger.debug(
52
+ f"Background: Resolved relations for entity {entity_permalink} (id={entity_id})"
53
+ )
54
+ except Exception as e:
55
+ # Log but don't fail - this is a background task
56
+ logger.warning(
57
+ f"Background: Failed to resolve relations for entity {entity_permalink}: {e}"
58
+ )
59
+
60
+
61
+ ## Create endpoints
62
+
63
+
64
+ @router.post("/entities", response_model=EntityResponse)
65
+ async def create_entity(
66
+ data: Entity,
67
+ background_tasks: BackgroundTasks,
68
+ entity_service: EntityServiceDep,
69
+ search_service: SearchServiceDep,
70
+ ) -> EntityResponse:
71
+ """Create an entity."""
72
+ logger.info(
73
+ "API request", endpoint="create_entity", entity_type=data.entity_type, title=data.title
74
+ )
75
+
76
+ entity = await entity_service.create_entity(data)
77
+
78
+ # reindex
79
+ await search_service.index_entity(entity, background_tasks=background_tasks)
80
+ result = EntityResponse.model_validate(entity)
81
+
82
+ logger.info(
83
+ f"API response: endpoint='create_entity' title={result.title}, permalink={result.permalink}, status_code=201"
84
+ )
85
+ return result
86
+
87
+
88
+ @router.put("/entities/{permalink:path}", response_model=EntityResponse)
89
+ async def create_or_update_entity(
90
+ project: ProjectPathDep,
91
+ permalink: Permalink,
92
+ data: Entity,
93
+ response: Response,
94
+ background_tasks: BackgroundTasks,
95
+ entity_service: EntityServiceDep,
96
+ search_service: SearchServiceDep,
97
+ file_service: FileServiceDep,
98
+ sync_service: SyncServiceDep,
99
+ ) -> EntityResponse:
100
+ """Create or update an entity. If entity exists, it will be updated, otherwise created."""
101
+ logger.info(
102
+ f"API request: create_or_update_entity for {project=}, {permalink=}, {data.entity_type=}, {data.title=}"
103
+ )
104
+
105
+ # Validate permalink matches
106
+ if data.permalink != permalink:
107
+ logger.warning(
108
+ f"API validation error: creating/updating entity with permalink mismatch - url={permalink}, data={data.permalink}",
109
+ )
110
+ raise HTTPException(
111
+ status_code=400,
112
+ detail=f"Entity permalink {data.permalink} must match URL path: '{permalink}'",
113
+ )
114
+
115
+ # Try create_or_update operation
116
+ entity, created = await entity_service.create_or_update_entity(data)
117
+ response.status_code = 201 if created else 200
118
+
119
+ # reindex
120
+ await search_service.index_entity(entity, background_tasks=background_tasks)
121
+
122
+ # Schedule relation resolution as a background task for new entities
123
+ # This prevents blocking the API response while resolving potentially many relations
124
+ if created:
125
+ background_tasks.add_task(
126
+ resolve_relations_background, sync_service, entity.id, entity.permalink or ""
127
+ )
128
+
129
+ result = EntityResponse.model_validate(entity)
130
+
131
+ logger.info(
132
+ f"API response: {result.title=}, {result.permalink=}, {created=}, status_code={response.status_code}"
133
+ )
134
+ return result
135
+
136
+
137
+ @router.patch("/entities/{identifier:path}", response_model=EntityResponse)
138
+ async def edit_entity(
139
+ identifier: str,
140
+ data: EditEntityRequest,
141
+ background_tasks: BackgroundTasks,
142
+ entity_service: EntityServiceDep,
143
+ search_service: SearchServiceDep,
144
+ ) -> EntityResponse:
145
+ """Edit an existing entity using various operations like append, prepend, find_replace, or replace_section.
146
+
147
+ This endpoint allows for targeted edits without requiring the full entity content.
148
+ """
149
+ logger.info(
150
+ f"API request: endpoint='edit_entity', identifier='{identifier}', operation='{data.operation}'"
151
+ )
152
+
153
+ try:
154
+ # Edit the entity using the service
155
+ entity = await entity_service.edit_entity(
156
+ identifier=identifier,
157
+ operation=data.operation,
158
+ content=data.content,
159
+ section=data.section,
160
+ find_text=data.find_text,
161
+ expected_replacements=data.expected_replacements,
162
+ )
163
+
164
+ # Reindex the updated entity
165
+ await search_service.index_entity(entity, background_tasks=background_tasks)
166
+
167
+ # Return the updated entity response
168
+ result = EntityResponse.model_validate(entity)
169
+
170
+ logger.info(
171
+ "API response",
172
+ endpoint="edit_entity",
173
+ identifier=identifier,
174
+ operation=data.operation,
175
+ permalink=result.permalink,
176
+ status_code=200,
177
+ )
178
+
179
+ return result
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error editing entity: {e}")
183
+ raise HTTPException(status_code=400, detail=str(e))
184
+
185
+
186
+ @router.post("/move")
187
+ async def move_entity(
188
+ data: MoveEntityRequest,
189
+ background_tasks: BackgroundTasks,
190
+ entity_service: EntityServiceDep,
191
+ project_config: ProjectConfigDep,
192
+ app_config: AppConfigDep,
193
+ search_service: SearchServiceDep,
194
+ ) -> EntityResponse:
195
+ """Move an entity to a new file location with project consistency.
196
+
197
+ This endpoint moves a note to a different path while maintaining project
198
+ consistency and optionally updating permalinks based on configuration.
199
+ """
200
+ logger.info(
201
+ f"API request: endpoint='move_entity', identifier='{data.identifier}', destination='{data.destination_path}'"
202
+ )
203
+
204
+ try:
205
+ # Move the entity using the service
206
+ moved_entity = await entity_service.move_entity(
207
+ identifier=data.identifier,
208
+ destination_path=data.destination_path,
209
+ project_config=project_config,
210
+ app_config=app_config,
211
+ )
212
+
213
+ # Get the moved entity to reindex it
214
+ entity = await entity_service.link_resolver.resolve_link(data.destination_path)
215
+ if entity:
216
+ await search_service.index_entity(entity, background_tasks=background_tasks)
217
+
218
+ logger.info(
219
+ "API response",
220
+ endpoint="move_entity",
221
+ identifier=data.identifier,
222
+ destination=data.destination_path,
223
+ status_code=200,
224
+ )
225
+ result = EntityResponse.model_validate(moved_entity)
226
+ return result
227
+
228
+ except Exception as e:
229
+ logger.error(f"Error moving entity: {e}")
230
+ raise HTTPException(status_code=400, detail=str(e))
231
+
232
+
233
+ ## Read endpoints
234
+
235
+
236
+ @router.get("/entities/{identifier:path}", response_model=EntityResponse)
237
+ async def get_entity(
238
+ entity_service: EntityServiceDep,
239
+ link_resolver: LinkResolverDep,
240
+ identifier: str,
241
+ ) -> EntityResponse:
242
+ """Get a specific entity by file path or permalink..
243
+
244
+ Args:
245
+ identifier: Entity file path or permalink
246
+ :param entity_service: EntityService
247
+ :param link_resolver: LinkResolver
248
+ """
249
+ logger.info(f"request: get_entity with identifier={identifier}")
250
+ entity = await link_resolver.resolve_link(identifier)
251
+ if not entity:
252
+ raise HTTPException(status_code=404, detail=f"Entity {identifier} not found")
253
+
254
+ result = EntityResponse.model_validate(entity)
255
+ return result
256
+
257
+
258
+ @router.get("/entities", response_model=EntityListResponse)
259
+ async def get_entities(
260
+ entity_service: EntityServiceDep,
261
+ permalink: Annotated[list[str] | None, Query()] = None,
262
+ ) -> EntityListResponse:
263
+ """Open specific entities"""
264
+ logger.info(f"request: get_entities with permalinks={permalink}")
265
+
266
+ entities = await entity_service.get_entities_by_permalinks(permalink) if permalink else []
267
+ result = EntityListResponse(
268
+ entities=[EntityResponse.model_validate(entity) for entity in entities]
269
+ )
270
+ return result
271
+
272
+
273
+ ## Delete endpoints
274
+
275
+
276
+ @router.delete("/entities/{identifier:path}", response_model=DeleteEntitiesResponse)
277
+ async def delete_entity(
278
+ identifier: str,
279
+ background_tasks: BackgroundTasks,
280
+ entity_service: EntityServiceDep,
281
+ link_resolver: LinkResolverDep,
282
+ search_service=Depends(get_search_service),
283
+ ) -> DeleteEntitiesResponse:
284
+ """Delete a single entity and remove from search index."""
285
+ logger.info(f"request: delete_entity with identifier={identifier}")
286
+
287
+ entity = await link_resolver.resolve_link(identifier)
288
+ if entity is None:
289
+ return DeleteEntitiesResponse(deleted=False)
290
+
291
+ # Delete the entity
292
+ deleted = await entity_service.delete_entity(entity.permalink or entity.id)
293
+
294
+ # Remove from search index (entity, observations, and relations)
295
+ background_tasks.add_task(search_service.handle_delete, entity)
296
+
297
+ result = DeleteEntitiesResponse(deleted=deleted)
298
+ return result
299
+
300
+
301
+ @router.post("/entities/delete", response_model=DeleteEntitiesResponse)
302
+ async def delete_entities(
303
+ data: DeleteEntitiesRequest,
304
+ background_tasks: BackgroundTasks,
305
+ entity_service: EntityServiceDep,
306
+ search_service=Depends(get_search_service),
307
+ ) -> DeleteEntitiesResponse:
308
+ """Delete entities and remove from search index."""
309
+ logger.info(f"request: delete_entities with data={data}")
310
+ deleted = False
311
+
312
+ # Remove each deleted entity from search index
313
+ for permalink in data.permalinks:
314
+ deleted = await entity_service.delete_entity(permalink)
315
+ background_tasks.add_task(search_service.delete_by_permalink, permalink)
316
+
317
+ result = DeleteEntitiesResponse(deleted=deleted)
318
+ return result
@@ -0,0 +1,80 @@
1
+ """Management router for basic-memory API."""
2
+
3
+ import asyncio
4
+
5
+ from fastapi import APIRouter, Request
6
+ from loguru import logger
7
+ from pydantic import BaseModel
8
+
9
+ from basic_memory.config import ConfigManager
10
+ from basic_memory.deps import SyncServiceDep, ProjectRepositoryDep
11
+
12
+ router = APIRouter(prefix="/management", tags=["management"])
13
+
14
+
15
+ class WatchStatusResponse(BaseModel):
16
+ """Response model for watch status."""
17
+
18
+ running: bool
19
+ """Whether the watch service is currently running."""
20
+
21
+
22
+ @router.get("/watch/status", response_model=WatchStatusResponse)
23
+ async def get_watch_status(request: Request) -> WatchStatusResponse:
24
+ """Get the current status of the watch service."""
25
+ return WatchStatusResponse(
26
+ running=request.app.state.watch_task is not None and not request.app.state.watch_task.done()
27
+ )
28
+
29
+
30
+ @router.post("/watch/start", response_model=WatchStatusResponse)
31
+ async def start_watch_service(
32
+ request: Request, project_repository: ProjectRepositoryDep, sync_service: SyncServiceDep
33
+ ) -> WatchStatusResponse:
34
+ """Start the watch service if it's not already running."""
35
+
36
+ # needed because of circular imports from sync -> app
37
+ from basic_memory.sync import WatchService
38
+ from basic_memory.sync.background_sync import create_background_sync_task
39
+
40
+ if request.app.state.watch_task is not None and not request.app.state.watch_task.done():
41
+ # Watch service is already running
42
+ return WatchStatusResponse(running=True)
43
+
44
+ app_config = ConfigManager().config
45
+
46
+ # Create and start a new watch service
47
+ logger.info("Starting watch service via management API")
48
+
49
+ # Get services needed for the watch task
50
+ watch_service = WatchService(
51
+ app_config=app_config,
52
+ project_repository=project_repository,
53
+ )
54
+
55
+ # Create and store the task
56
+ watch_task = create_background_sync_task(sync_service, watch_service)
57
+ request.app.state.watch_task = watch_task
58
+
59
+ return WatchStatusResponse(running=True)
60
+
61
+
62
+ @router.post("/watch/stop", response_model=WatchStatusResponse)
63
+ async def stop_watch_service(request: Request) -> WatchStatusResponse: # pragma: no cover
64
+ """Stop the watch service if it's running."""
65
+ if request.app.state.watch_task is None or request.app.state.watch_task.done():
66
+ # Watch service is not running
67
+ return WatchStatusResponse(running=False)
68
+
69
+ # Cancel the running task
70
+ logger.info("Stopping watch service via management API")
71
+ request.app.state.watch_task.cancel()
72
+
73
+ # Wait for it to be properly cancelled
74
+ try:
75
+ await request.app.state.watch_task
76
+ except asyncio.CancelledError:
77
+ pass
78
+
79
+ request.app.state.watch_task = None
80
+ return WatchStatusResponse(running=False)
@@ -0,0 +1,90 @@
1
+ """Routes for memory:// URI operations."""
2
+
3
+ from typing import Annotated, Optional
4
+
5
+ from fastapi import APIRouter, Query
6
+ from loguru import logger
7
+
8
+ from basic_memory.deps import ContextServiceDep, EntityRepositoryDep
9
+ from basic_memory.schemas.base import TimeFrame, parse_timeframe
10
+ from basic_memory.schemas.memory import (
11
+ GraphContext,
12
+ normalize_memory_url,
13
+ )
14
+ from basic_memory.schemas.search import SearchItemType
15
+ from basic_memory.api.routers.utils import to_graph_context
16
+
17
+ router = APIRouter(prefix="/memory", tags=["memory"])
18
+
19
+
20
+ @router.get("/recent", response_model=GraphContext)
21
+ async def recent(
22
+ context_service: ContextServiceDep,
23
+ entity_repository: EntityRepositoryDep,
24
+ type: Annotated[list[SearchItemType] | None, Query()] = None,
25
+ depth: int = 1,
26
+ timeframe: TimeFrame = "7d",
27
+ page: int = 1,
28
+ page_size: int = 10,
29
+ max_related: int = 10,
30
+ ) -> GraphContext:
31
+ # return all types by default
32
+ types = (
33
+ [SearchItemType.ENTITY, SearchItemType.RELATION, SearchItemType.OBSERVATION]
34
+ if not type
35
+ else type
36
+ )
37
+
38
+ logger.debug(
39
+ f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
40
+ )
41
+ # Parse timeframe
42
+ since = parse_timeframe(timeframe)
43
+ limit = page_size
44
+ offset = (page - 1) * page_size
45
+
46
+ # Build context
47
+ context = await context_service.build_context(
48
+ types=types, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
49
+ )
50
+ recent_context = await to_graph_context(
51
+ context, entity_repository=entity_repository, page=page, page_size=page_size
52
+ )
53
+ logger.debug(f"Recent context: {recent_context.model_dump_json()}")
54
+ return recent_context
55
+
56
+
57
+ # get_memory_context needs to be declared last so other paths can match
58
+
59
+
60
+ @router.get("/{uri:path}", response_model=GraphContext)
61
+ async def get_memory_context(
62
+ context_service: ContextServiceDep,
63
+ entity_repository: EntityRepositoryDep,
64
+ uri: str,
65
+ depth: int = 1,
66
+ timeframe: Optional[TimeFrame] = None,
67
+ page: int = 1,
68
+ page_size: int = 10,
69
+ max_related: int = 10,
70
+ ) -> GraphContext:
71
+ """Get rich context from memory:// URI."""
72
+ # add the project name from the config to the url as the "host
73
+ # Parse URI
74
+ logger.debug(
75
+ f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
76
+ )
77
+ memory_url = normalize_memory_url(uri)
78
+
79
+ # Parse timeframe
80
+ since = parse_timeframe(timeframe) if timeframe else None
81
+ limit = page_size
82
+ offset = (page - 1) * page_size
83
+
84
+ # Build context
85
+ context = await context_service.build_context(
86
+ memory_url, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
87
+ )
88
+ return await to_graph_context(
89
+ context, entity_repository=entity_repository, page=page, page_size=page_size
90
+ )