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.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +27 -3
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/api/app.py +63 -31
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +165 -28
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +28 -67
- basic_memory/api/routers/project_router.py +406 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +219 -14
- basic_memory/api/routers/search_router.py +21 -13
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +52 -1
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +13 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +51 -0
- basic_memory/cli/commands/db.py +26 -7
- basic_memory/cli/commands/import_chatgpt.py +83 -0
- basic_memory/cli/commands/import_claude_conversations.py +86 -0
- basic_memory/cli/commands/import_claude_projects.py +85 -0
- basic_memory/cli/commands/import_memory_json.py +35 -92
- basic_memory/cli/commands/mcp.py +84 -10
- basic_memory/cli/commands/project.py +876 -0
- basic_memory/cli/commands/status.py +47 -30
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +13 -6
- basic_memory/config.py +481 -22
- basic_memory/db.py +192 -32
- basic_memory/deps.py +252 -22
- basic_memory/file_utils.py +113 -58
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +177 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +143 -23
- basic_memory/markdown/markdown_processor.py +3 -3
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +28 -13
- basic_memory/mcp/async_client.py +134 -4
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +7 -13
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +130 -0
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +225 -0
- basic_memory/mcp/tools/edit_note.py +320 -0
- basic_memory/mcp/tools/list_directory.py +167 -0
- basic_memory/mcp/tools/move_note.py +545 -0
- basic_memory/mcp/tools/project_management.py +200 -0
- basic_memory/mcp/tools/read_content.py +271 -0
- basic_memory/mcp/tools/read_note.py +255 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +369 -14
- basic_memory/mcp/tools/utils.py +374 -16
- basic_memory/mcp/tools/view_note.py +77 -0
- basic_memory/mcp/tools/write_note.py +207 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +67 -15
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +10 -6
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +229 -7
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +103 -0
- basic_memory/repository/relation_repository.py +21 -2
- basic_memory/repository/repository.py +147 -29
- basic_memory/repository/search_repository.py +437 -59
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +97 -8
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +188 -23
- basic_memory/schemas/project_info.py +211 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +57 -3
- basic_memory/schemas/response.py +9 -1
- basic_memory/schemas/search.py +33 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +251 -106
- basic_memory/services/directory_service.py +295 -0
- basic_memory/services/entity_service.py +595 -60
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +284 -30
- basic_memory/services/initialization.py +191 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +863 -0
- basic_memory/services/search_service.py +172 -34
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1176 -96
- basic_memory/sync/watch_service.py +412 -135
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +388 -28
- basic_memory-0.16.1.dist-info/METADATA +493 -0
- basic_memory-0.16.1.dist-info/RECORD +148 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -203
- basic_memory/mcp/tools/knowledge.py +0 -56
- basic_memory/mcp/tools/memory.py +0 -151
- basic_memory/mcp/tools/notes.py +0 -122
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -34
- basic_memory-0.2.12.dist-info/METADATA +0 -291
- basic_memory-0.2.12.dist-info/RECORD +0 -78
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
- {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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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/{
|
|
225
|
+
@router.get("/entities/{identifier:path}", response_model=EntityResponse)
|
|
82
226
|
async def get_entity(
|
|
83
227
|
entity_service: EntityServiceDep,
|
|
84
|
-
|
|
228
|
+
link_resolver: LinkResolverDep,
|
|
229
|
+
identifier: str,
|
|
85
230
|
) -> EntityResponse:
|
|
86
|
-
"""Get a specific entity by
|
|
231
|
+
"""Get a specific entity by file path or permalink..
|
|
87
232
|
|
|
88
233
|
Args:
|
|
89
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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}`
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
112
|
-
|
|
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}`
|
|
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 =
|
|
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,
|
|
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)
|