basic-memory 0.2.12__py3-none-any.whl → 0.16.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.

Potentially problematic release.


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

Files changed (149) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +63 -31
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +165 -28
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +28 -67
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +219 -14
  23. basic_memory/api/routers/search_router.py +21 -13
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +52 -1
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +26 -7
  41. basic_memory/cli/commands/import_chatgpt.py +83 -0
  42. basic_memory/cli/commands/import_claude_conversations.py +86 -0
  43. basic_memory/cli/commands/import_claude_projects.py +85 -0
  44. basic_memory/cli/commands/import_memory_json.py +35 -92
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +47 -30
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +13 -6
  50. basic_memory/config.py +481 -22
  51. basic_memory/db.py +192 -32
  52. basic_memory/deps.py +252 -22
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -14
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +437 -59
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +188 -23
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +57 -3
  115. basic_memory/schemas/response.py +9 -1
  116. basic_memory/schemas/search.py +33 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +251 -106
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +595 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +50 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +172 -34
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1176 -96
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +388 -28
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -203
  140. basic_memory/mcp/tools/knowledge.py +0 -56
  141. basic_memory/mcp/tools/memory.py +0 -151
  142. basic_memory/mcp/tools/notes.py +0 -122
  143. basic_memory/schemas/discovery.py +0 -28
  144. basic_memory/sync/file_change_scanner.py +0 -158
  145. basic_memory/sync/utils.py +0 -34
  146. basic_memory-0.2.12.dist-info/METADATA +0 -291
  147. basic_memory-0.2.12.dist-info/RECORD +0 -78
  148. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  149. {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -10,6 +10,11 @@ from basic_memory.deps import (
10
10
  get_search_service,
11
11
  SearchServiceDep,
12
12
  LinkResolverDep,
13
+ ProjectPathDep,
14
+ FileServiceDep,
15
+ ProjectConfigDep,
16
+ AppConfigDep,
17
+ SyncServiceDep,
13
18
  )
14
19
  from basic_memory.schemas import (
15
20
  EntityListResponse,
@@ -17,11 +22,31 @@ from basic_memory.schemas import (
17
22
  DeleteEntitiesResponse,
18
23
  DeleteEntitiesRequest,
19
24
  )
25
+ from basic_memory.schemas.request import EditEntityRequest, MoveEntityRequest
20
26
  from basic_memory.schemas.base import Permalink, Entity
21
- from basic_memory.services.exceptions import EntityNotFoundError
22
27
 
23
28
  router = APIRouter(prefix="/knowledge", tags=["knowledge"])
24
29
 
30
+
31
+ async def resolve_relations_background(sync_service, entity_id: int, entity_permalink: str) -> None:
32
+ """Background task to resolve relations for a specific entity.
33
+
34
+ This runs asynchronously after the API response is sent, preventing
35
+ long delays when creating entities with many relations.
36
+ """
37
+ try:
38
+ # Only resolve relations for the newly created entity
39
+ await sync_service.resolve_relations(entity_id=entity_id)
40
+ logger.debug(
41
+ f"Background: Resolved relations for entity {entity_permalink} (id={entity_id})"
42
+ )
43
+ except Exception as e:
44
+ # Log but don't fail - this is a background task
45
+ logger.warning(
46
+ f"Background: Failed to resolve relations for entity {entity_permalink}: {e}"
47
+ )
48
+
49
+
25
50
  ## Create endpoints
26
51
 
27
52
 
@@ -33,7 +58,9 @@ async def create_entity(
33
58
  search_service: SearchServiceDep,
34
59
  ) -> EntityResponse:
35
60
  """Create an entity."""
36
- logger.info(f"request: create_entity with data={data}")
61
+ logger.info(
62
+ "API request", endpoint="create_entity", entity_type=data.entity_type, title=data.title
63
+ )
37
64
 
38
65
  entity = await entity_service.create_entity(data)
39
66
 
@@ -41,25 +68,38 @@ async def create_entity(
41
68
  await search_service.index_entity(entity, background_tasks=background_tasks)
42
69
  result = EntityResponse.model_validate(entity)
43
70
 
44
- logger.info(f"response: create_entity with result={result}")
71
+ logger.info(
72
+ f"API response: endpoint='create_entity' title={result.title}, permalink={result.permalink}, status_code=201"
73
+ )
45
74
  return result
46
75
 
47
76
 
48
77
  @router.put("/entities/{permalink:path}", response_model=EntityResponse)
49
78
  async def create_or_update_entity(
79
+ project: ProjectPathDep,
50
80
  permalink: Permalink,
51
81
  data: Entity,
52
82
  response: Response,
53
83
  background_tasks: BackgroundTasks,
54
84
  entity_service: EntityServiceDep,
55
85
  search_service: SearchServiceDep,
86
+ file_service: FileServiceDep,
87
+ sync_service: SyncServiceDep,
56
88
  ) -> EntityResponse:
57
89
  """Create or update an entity. If entity exists, it will be updated, otherwise created."""
58
- logger.info(f"request: create_or_update_entity with permalink={permalink}, data={data}")
90
+ logger.info(
91
+ f"API request: create_or_update_entity for {project=}, {permalink=}, {data.entity_type=}, {data.title=}"
92
+ )
59
93
 
60
94
  # Validate permalink matches
61
95
  if data.permalink != permalink:
62
- raise HTTPException(status_code=400, detail="Entity permalink must match URL path")
96
+ logger.warning(
97
+ f"API validation error: creating/updating entity with permalink mismatch - url={permalink}, data={data.permalink}",
98
+ )
99
+ raise HTTPException(
100
+ status_code=400,
101
+ detail=f"Entity permalink {data.permalink} must match URL path: '{permalink}'",
102
+ )
63
103
 
64
104
  # Try create_or_update operation
65
105
  entity, created = await entity_service.create_or_update_entity(data)
@@ -67,39 +107,141 @@ async def create_or_update_entity(
67
107
 
68
108
  # reindex
69
109
  await search_service.index_entity(entity, background_tasks=background_tasks)
110
+
111
+ # Schedule relation resolution as a background task for new entities
112
+ # This prevents blocking the API response while resolving potentially many relations
113
+ if created:
114
+ background_tasks.add_task(
115
+ resolve_relations_background, sync_service, entity.id, entity.permalink or ""
116
+ )
117
+
70
118
  result = EntityResponse.model_validate(entity)
71
119
 
72
120
  logger.info(
73
- f"response: create_or_update_entity with result={result}, status_code={response.status_code}"
121
+ f"API response: {result.title=}, {result.permalink=}, {created=}, status_code={response.status_code}"
74
122
  )
75
123
  return result
76
124
 
77
125
 
126
+ @router.patch("/entities/{identifier:path}", response_model=EntityResponse)
127
+ async def edit_entity(
128
+ identifier: str,
129
+ data: EditEntityRequest,
130
+ background_tasks: BackgroundTasks,
131
+ entity_service: EntityServiceDep,
132
+ search_service: SearchServiceDep,
133
+ ) -> EntityResponse:
134
+ """Edit an existing entity using various operations like append, prepend, find_replace, or replace_section.
135
+
136
+ This endpoint allows for targeted edits without requiring the full entity content.
137
+ """
138
+ logger.info(
139
+ f"API request: endpoint='edit_entity', identifier='{identifier}', operation='{data.operation}'"
140
+ )
141
+
142
+ try:
143
+ # Edit the entity using the service
144
+ entity = await entity_service.edit_entity(
145
+ identifier=identifier,
146
+ operation=data.operation,
147
+ content=data.content,
148
+ section=data.section,
149
+ find_text=data.find_text,
150
+ expected_replacements=data.expected_replacements,
151
+ )
152
+
153
+ # Reindex the updated entity
154
+ await search_service.index_entity(entity, background_tasks=background_tasks)
155
+
156
+ # Return the updated entity response
157
+ result = EntityResponse.model_validate(entity)
158
+
159
+ logger.info(
160
+ "API response",
161
+ endpoint="edit_entity",
162
+ identifier=identifier,
163
+ operation=data.operation,
164
+ permalink=result.permalink,
165
+ status_code=200,
166
+ )
167
+
168
+ return result
169
+
170
+ except Exception as e:
171
+ logger.error(f"Error editing entity: {e}")
172
+ raise HTTPException(status_code=400, detail=str(e))
173
+
174
+
175
+ @router.post("/move")
176
+ async def move_entity(
177
+ data: MoveEntityRequest,
178
+ background_tasks: BackgroundTasks,
179
+ entity_service: EntityServiceDep,
180
+ project_config: ProjectConfigDep,
181
+ app_config: AppConfigDep,
182
+ search_service: SearchServiceDep,
183
+ ) -> EntityResponse:
184
+ """Move an entity to a new file location with project consistency.
185
+
186
+ This endpoint moves a note to a different path while maintaining project
187
+ consistency and optionally updating permalinks based on configuration.
188
+ """
189
+ logger.info(
190
+ f"API request: endpoint='move_entity', identifier='{data.identifier}', destination='{data.destination_path}'"
191
+ )
192
+
193
+ try:
194
+ # Move the entity using the service
195
+ moved_entity = await entity_service.move_entity(
196
+ identifier=data.identifier,
197
+ destination_path=data.destination_path,
198
+ project_config=project_config,
199
+ app_config=app_config,
200
+ )
201
+
202
+ # Get the moved entity to reindex it
203
+ entity = await entity_service.link_resolver.resolve_link(data.destination_path)
204
+ if entity:
205
+ await search_service.index_entity(entity, background_tasks=background_tasks)
206
+
207
+ logger.info(
208
+ "API response",
209
+ endpoint="move_entity",
210
+ identifier=data.identifier,
211
+ destination=data.destination_path,
212
+ status_code=200,
213
+ )
214
+ result = EntityResponse.model_validate(moved_entity)
215
+ return result
216
+
217
+ except Exception as e:
218
+ logger.error(f"Error moving entity: {e}")
219
+ raise HTTPException(status_code=400, detail=str(e))
220
+
221
+
78
222
  ## Read endpoints
79
223
 
80
224
 
81
- @router.get("/entities/{permalink:path}", response_model=EntityResponse)
225
+ @router.get("/entities/{identifier:path}", response_model=EntityResponse)
82
226
  async def get_entity(
83
227
  entity_service: EntityServiceDep,
84
- permalink: str,
228
+ link_resolver: LinkResolverDep,
229
+ identifier: str,
85
230
  ) -> EntityResponse:
86
- """Get a specific entity by ID.
231
+ """Get a specific entity by file path or permalink..
87
232
 
88
233
  Args:
89
- permalink: Entity path ID
90
- content: If True, include full file content
234
+ identifier: Entity file path or permalink
91
235
  :param entity_service: EntityService
236
+ :param link_resolver: LinkResolver
92
237
  """
93
- logger.info(f"request: get_entity with permalink={permalink}")
94
- try:
95
- entity = await entity_service.get_by_permalink(permalink)
96
- result = EntityResponse.model_validate(entity)
238
+ logger.info(f"request: get_entity with identifier={identifier}")
239
+ entity = await link_resolver.resolve_link(identifier)
240
+ if not entity:
241
+ raise HTTPException(status_code=404, detail=f"Entity {identifier} not found")
97
242
 
98
- logger.info(f"response: get_entity with result={result}")
99
- return result
100
- except EntityNotFoundError:
101
- logger.error(f"Error: Entity with {permalink} not found")
102
- raise HTTPException(status_code=404, detail=f"Entity with {permalink} not found")
243
+ result = EntityResponse.model_validate(entity)
244
+ return result
103
245
 
104
246
 
105
247
  @router.get("/entities", response_model=EntityListResponse)
@@ -114,8 +256,6 @@ async def get_entities(
114
256
  result = EntityListResponse(
115
257
  entities=[EntityResponse.model_validate(entity) for entity in entities]
116
258
  )
117
-
118
- logger.info(f"response: get_entities with result={result}")
119
259
  return result
120
260
 
121
261
 
@@ -135,17 +275,15 @@ async def delete_entity(
135
275
 
136
276
  entity = await link_resolver.resolve_link(identifier)
137
277
  if entity is None:
138
- logger.info("response: delete_entity with result=DeleteEntitiesResponse(deleted=False)")
139
278
  return DeleteEntitiesResponse(deleted=False)
140
279
 
141
280
  # Delete the entity
142
- deleted = await entity_service.delete_entity(entity.permalink)
281
+ deleted = await entity_service.delete_entity(entity.permalink or entity.id)
143
282
 
144
- # Remove from search index
145
- background_tasks.add_task(search_service.delete_by_permalink, entity.permalink)
283
+ # Remove from search index (entity, observations, and relations)
284
+ background_tasks.add_task(search_service.handle_delete, entity)
146
285
 
147
286
  result = DeleteEntitiesResponse(deleted=deleted)
148
- logger.info(f"response: delete_entity with result={result}")
149
287
  return result
150
288
 
151
289
 
@@ -166,5 +304,4 @@ async def delete_entities(
166
304
  background_tasks.add_task(search_service.delete_by_permalink, permalink)
167
305
 
168
306
  result = DeleteEntitiesResponse(deleted=deleted)
169
- logger.info(f"response: delete_entities with result={result}")
170
307
  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)
@@ -1,75 +1,22 @@
1
1
  """Routes for memory:// URI operations."""
2
2
 
3
- from typing import Annotated
3
+ from typing import Annotated, Optional
4
4
 
5
- from dateparser import parse
6
5
  from fastapi import APIRouter, Query
7
6
  from loguru import logger
8
7
 
9
8
  from basic_memory.deps import ContextServiceDep, EntityRepositoryDep
10
- from basic_memory.repository import EntityRepository
11
- from basic_memory.repository.search_repository import SearchIndexRow
12
- from basic_memory.schemas.base import TimeFrame
9
+ from basic_memory.schemas.base import TimeFrame, parse_timeframe
13
10
  from basic_memory.schemas.memory import (
14
11
  GraphContext,
15
- RelationSummary,
16
- EntitySummary,
17
- ObservationSummary,
18
- MemoryMetadata,
19
12
  normalize_memory_url,
20
13
  )
21
14
  from basic_memory.schemas.search import SearchItemType
22
- from basic_memory.services.context_service import ContextResultRow
15
+ from basic_memory.api.routers.utils import to_graph_context
23
16
 
24
17
  router = APIRouter(prefix="/memory", tags=["memory"])
25
18
 
26
19
 
27
- async def to_graph_context(context, entity_repository: EntityRepository):
28
- # return results
29
- async def to_summary(item: SearchIndexRow | ContextResultRow):
30
- match item.type:
31
- case SearchItemType.ENTITY:
32
- assert item.title is not None
33
- assert item.created_at is not None
34
-
35
- return EntitySummary(
36
- title=item.title,
37
- permalink=item.permalink,
38
- file_path=item.file_path,
39
- created_at=item.created_at,
40
- )
41
- case SearchItemType.OBSERVATION:
42
- assert item.category is not None
43
- assert item.content is not None
44
-
45
- return ObservationSummary(
46
- category=item.category, content=item.content, permalink=item.permalink
47
- )
48
- case SearchItemType.RELATION:
49
- assert item.from_id is not None
50
- from_entity = await entity_repository.find_by_id(item.from_id)
51
- assert from_entity is not None
52
-
53
- to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
54
-
55
- return RelationSummary(
56
- permalink=item.permalink,
57
- relation_type=item.type,
58
- from_id=from_entity.permalink,
59
- to_id=to_entity.permalink if to_entity else None,
60
- )
61
- case _: # pragma: no cover
62
- raise ValueError(f"Unexpected type: {item.type}")
63
-
64
- primary_results = [await to_summary(r) for r in context["primary_results"]]
65
- related_results = [await to_summary(r) for r in context["related_results"]]
66
- metadata = MemoryMetadata.model_validate(context["metadata"])
67
- # Transform to GraphContext
68
- return GraphContext(
69
- primary_results=primary_results, related_results=related_results, metadata=metadata
70
- )
71
-
72
-
73
20
  @router.get("/recent", response_model=GraphContext)
74
21
  async def recent(
75
22
  context_service: ContextServiceDep,
@@ -77,7 +24,9 @@ async def recent(
77
24
  type: Annotated[list[SearchItemType] | None, Query()] = None,
78
25
  depth: int = 1,
79
26
  timeframe: TimeFrame = "7d",
80
- max_results: int = 10,
27
+ page: int = 1,
28
+ page_size: int = 10,
29
+ max_related: int = 10,
81
30
  ) -> GraphContext:
82
31
  # return all types by default
83
32
  types = (
@@ -87,16 +36,22 @@ async def recent(
87
36
  )
88
37
 
89
38
  logger.debug(
90
- f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` max_results: `{max_results}`"
39
+ f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
91
40
  )
92
41
  # Parse timeframe
93
- since = parse(timeframe)
42
+ since = parse_timeframe(timeframe)
43
+ limit = page_size
44
+ offset = (page - 1) * page_size
94
45
 
95
46
  # Build context
96
47
  context = await context_service.build_context(
97
- types=types, depth=depth, since=since, max_results=max_results
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
98
52
  )
99
- return await to_graph_context(context, entity_repository=entity_repository)
53
+ logger.debug(f"Recent context: {recent_context.model_dump_json()}")
54
+ return recent_context
100
55
 
101
56
 
102
57
  # get_memory_context needs to be declared last so other paths can match
@@ -108,22 +63,28 @@ async def get_memory_context(
108
63
  entity_repository: EntityRepositoryDep,
109
64
  uri: str,
110
65
  depth: int = 1,
111
- timeframe: TimeFrame = "7d",
112
- max_results: int = 10,
66
+ timeframe: Optional[TimeFrame] = None,
67
+ page: int = 1,
68
+ page_size: int = 10,
69
+ max_related: int = 10,
113
70
  ) -> GraphContext:
114
71
  """Get rich context from memory:// URI."""
115
72
  # add the project name from the config to the url as the "host
116
73
  # Parse URI
117
74
  logger.debug(
118
- f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` max_results: `{max_results}`"
75
+ f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
119
76
  )
120
77
  memory_url = normalize_memory_url(uri)
121
78
 
122
79
  # Parse timeframe
123
- since = parse(timeframe)
80
+ since = parse_timeframe(timeframe) if timeframe else None
81
+ limit = page_size
82
+ offset = (page - 1) * page_size
124
83
 
125
84
  # Build context
126
85
  context = await context_service.build_context(
127
- memory_url, depth=depth, since=since, max_results=max_results
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
128
90
  )
129
- return await to_graph_context(context, entity_repository=entity_repository)