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
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Service for search operations."""
|
|
2
2
|
|
|
3
|
+
import ast
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from typing import List, Optional, Set
|
|
5
6
|
|
|
7
|
+
from dateparser import parse
|
|
6
8
|
from fastapi import BackgroundTasks
|
|
7
9
|
from loguru import logger
|
|
10
|
+
from sqlalchemy import text
|
|
8
11
|
|
|
9
12
|
from basic_memory.models import Entity
|
|
10
13
|
from basic_memory.repository import EntityRepository
|
|
@@ -38,9 +41,10 @@ class SearchService:
|
|
|
38
41
|
|
|
39
42
|
async def reindex_all(self, background_tasks: Optional[BackgroundTasks] = None) -> None:
|
|
40
43
|
"""Reindex all content from database."""
|
|
41
|
-
logger.info("Starting full reindex")
|
|
42
44
|
|
|
45
|
+
logger.info("Starting full reindex")
|
|
43
46
|
# Clear and recreate search index
|
|
47
|
+
await self.repository.execute_query(text("DROP TABLE IF EXISTS search_index"), params={})
|
|
44
48
|
await self.init_search_index()
|
|
45
49
|
|
|
46
50
|
# Reindex all entities
|
|
@@ -51,7 +55,7 @@ class SearchService:
|
|
|
51
55
|
|
|
52
56
|
logger.info("Reindex complete")
|
|
53
57
|
|
|
54
|
-
async def search(self, query: SearchQuery) -> List[SearchIndexRow]:
|
|
58
|
+
async def search(self, query: SearchQuery, limit=10, offset=0) -> List[SearchIndexRow]:
|
|
55
59
|
"""Search across all indexed content.
|
|
56
60
|
|
|
57
61
|
Supports three modes:
|
|
@@ -63,27 +67,29 @@ class SearchService:
|
|
|
63
67
|
logger.debug("no criteria passed to query")
|
|
64
68
|
return []
|
|
65
69
|
|
|
66
|
-
logger.
|
|
70
|
+
logger.trace(f"Searching with query: {query}")
|
|
67
71
|
|
|
68
72
|
after_date = (
|
|
69
73
|
(
|
|
70
74
|
query.after_date
|
|
71
75
|
if isinstance(query.after_date, datetime)
|
|
72
|
-
else
|
|
76
|
+
else parse(query.after_date)
|
|
73
77
|
)
|
|
74
78
|
if query.after_date
|
|
75
79
|
else None
|
|
76
80
|
)
|
|
77
81
|
|
|
78
|
-
#
|
|
82
|
+
# search
|
|
79
83
|
results = await self.repository.search(
|
|
80
84
|
search_text=query.text,
|
|
81
85
|
permalink=query.permalink,
|
|
82
86
|
permalink_match=query.permalink_match,
|
|
83
87
|
title=query.title,
|
|
84
88
|
types=query.types,
|
|
85
|
-
|
|
89
|
+
search_item_types=query.entity_types,
|
|
86
90
|
after_date=after_date,
|
|
91
|
+
limit=limit,
|
|
92
|
+
offset=offset,
|
|
87
93
|
)
|
|
88
94
|
|
|
89
95
|
return results
|
|
@@ -107,15 +113,91 @@ class SearchService:
|
|
|
107
113
|
# Add word boundaries
|
|
108
114
|
variants.update(w.strip() for w in text.lower().split() if w.strip())
|
|
109
115
|
|
|
110
|
-
#
|
|
111
|
-
|
|
116
|
+
# Trigrams disabled: They create massive search index bloat, increasing DB size significantly
|
|
117
|
+
# and slowing down indexing performance. FTS5 search works well without them.
|
|
118
|
+
# See: https://github.com/basicmachines-co/basic-memory/issues/351
|
|
119
|
+
# variants.update(text[i : i + 3].lower() for i in range(len(text) - 2))
|
|
112
120
|
|
|
113
121
|
return variants
|
|
114
122
|
|
|
123
|
+
def _extract_entity_tags(self, entity: Entity) -> List[str]:
|
|
124
|
+
"""Extract tags from entity metadata for search indexing.
|
|
125
|
+
|
|
126
|
+
Handles multiple tag formats:
|
|
127
|
+
- List format: ["tag1", "tag2"]
|
|
128
|
+
- String format: "['tag1', 'tag2']" or "[tag1, tag2]"
|
|
129
|
+
- Empty: [] or "[]"
|
|
130
|
+
|
|
131
|
+
Returns a list of tag strings for search indexing.
|
|
132
|
+
"""
|
|
133
|
+
if not entity.entity_metadata or "tags" not in entity.entity_metadata:
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
tags = entity.entity_metadata["tags"]
|
|
137
|
+
|
|
138
|
+
# Handle list format (preferred)
|
|
139
|
+
if isinstance(tags, list):
|
|
140
|
+
return [str(tag) for tag in tags if tag]
|
|
141
|
+
|
|
142
|
+
# Handle string format (legacy)
|
|
143
|
+
if isinstance(tags, str):
|
|
144
|
+
try:
|
|
145
|
+
# Parse string representation of list
|
|
146
|
+
parsed_tags = ast.literal_eval(tags)
|
|
147
|
+
if isinstance(parsed_tags, list):
|
|
148
|
+
return [str(tag) for tag in parsed_tags if tag]
|
|
149
|
+
except (ValueError, SyntaxError):
|
|
150
|
+
# If parsing fails, treat as single tag
|
|
151
|
+
return [tags] if tags.strip() else []
|
|
152
|
+
|
|
153
|
+
return [] # pragma: no cover
|
|
154
|
+
|
|
115
155
|
async def index_entity(
|
|
116
156
|
self,
|
|
117
157
|
entity: Entity,
|
|
118
158
|
background_tasks: Optional[BackgroundTasks] = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
if background_tasks:
|
|
161
|
+
background_tasks.add_task(self.index_entity_data, entity)
|
|
162
|
+
else:
|
|
163
|
+
await self.index_entity_data(entity)
|
|
164
|
+
|
|
165
|
+
async def index_entity_data(
|
|
166
|
+
self,
|
|
167
|
+
entity: Entity,
|
|
168
|
+
) -> None:
|
|
169
|
+
# delete all search index data associated with entity
|
|
170
|
+
await self.repository.delete_by_entity_id(entity_id=entity.id)
|
|
171
|
+
|
|
172
|
+
# reindex
|
|
173
|
+
await self.index_entity_markdown(
|
|
174
|
+
entity
|
|
175
|
+
) if entity.is_markdown else await self.index_entity_file(entity)
|
|
176
|
+
|
|
177
|
+
async def index_entity_file(
|
|
178
|
+
self,
|
|
179
|
+
entity: Entity,
|
|
180
|
+
) -> None:
|
|
181
|
+
# Index entity file with no content
|
|
182
|
+
await self.repository.index_item(
|
|
183
|
+
SearchIndexRow(
|
|
184
|
+
id=entity.id,
|
|
185
|
+
entity_id=entity.id,
|
|
186
|
+
type=SearchItemType.ENTITY.value,
|
|
187
|
+
title=entity.title,
|
|
188
|
+
file_path=entity.file_path,
|
|
189
|
+
metadata={
|
|
190
|
+
"entity_type": entity.entity_type,
|
|
191
|
+
},
|
|
192
|
+
created_at=entity.created_at,
|
|
193
|
+
updated_at=entity.updated_at,
|
|
194
|
+
project_id=entity.project_id,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def index_entity_markdown(
|
|
199
|
+
self,
|
|
200
|
+
entity: Entity,
|
|
119
201
|
) -> None:
|
|
120
202
|
"""Index an entity and all its observations and relations.
|
|
121
203
|
|
|
@@ -123,67 +205,80 @@ class SearchService:
|
|
|
123
205
|
1. Entities
|
|
124
206
|
- permalink: direct from entity (e.g., "specs/search")
|
|
125
207
|
- file_path: physical file location
|
|
208
|
+
- project_id: project context for isolation
|
|
126
209
|
|
|
127
210
|
2. Observations
|
|
128
211
|
- permalink: entity permalink + /observations/id (e.g., "specs/search/observations/123")
|
|
129
212
|
- file_path: parent entity's file (where observation is defined)
|
|
213
|
+
- project_id: inherited from parent entity
|
|
130
214
|
|
|
131
215
|
3. Relations (only index outgoing relations defined in this file)
|
|
132
216
|
- permalink: from_entity/relation_type/to_entity (e.g., "specs/search/implements/features/search-ui")
|
|
133
217
|
- file_path: source entity's file (where relation is defined)
|
|
218
|
+
- project_id: inherited from source entity
|
|
134
219
|
|
|
135
220
|
Each type gets its own row in the search index with appropriate metadata.
|
|
221
|
+
The project_id is automatically added by the repository when indexing.
|
|
136
222
|
"""
|
|
137
|
-
if background_tasks:
|
|
138
|
-
background_tasks.add_task(self.index_entity_data, entity)
|
|
139
|
-
else:
|
|
140
|
-
await self.index_entity_data(entity)
|
|
141
223
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
entity: Entity,
|
|
145
|
-
) -> None:
|
|
146
|
-
"""Actually perform the indexing."""
|
|
224
|
+
# Collect all search index rows to batch insert at the end
|
|
225
|
+
rows_to_index = []
|
|
147
226
|
|
|
148
|
-
|
|
227
|
+
content_stems = []
|
|
228
|
+
content_snippet = ""
|
|
149
229
|
title_variants = self._generate_variants(entity.title)
|
|
150
|
-
|
|
230
|
+
content_stems.extend(title_variants)
|
|
151
231
|
|
|
152
232
|
content = await self.file_service.read_entity_content(entity)
|
|
153
233
|
if content:
|
|
154
|
-
|
|
234
|
+
content_stems.append(content)
|
|
235
|
+
content_snippet = f"{content[:250]}"
|
|
155
236
|
|
|
156
|
-
|
|
157
|
-
|
|
237
|
+
if entity.permalink:
|
|
238
|
+
content_stems.extend(self._generate_variants(entity.permalink))
|
|
158
239
|
|
|
159
|
-
|
|
240
|
+
content_stems.extend(self._generate_variants(entity.file_path))
|
|
160
241
|
|
|
161
|
-
#
|
|
162
|
-
|
|
242
|
+
# Add entity tags from frontmatter to search content
|
|
243
|
+
entity_tags = self._extract_entity_tags(entity)
|
|
244
|
+
if entity_tags:
|
|
245
|
+
content_stems.extend(entity_tags)
|
|
246
|
+
|
|
247
|
+
entity_content_stems = "\n".join(p for p in content_stems if p and p.strip())
|
|
248
|
+
|
|
249
|
+
# Add entity row
|
|
250
|
+
rows_to_index.append(
|
|
163
251
|
SearchIndexRow(
|
|
164
252
|
id=entity.id,
|
|
165
253
|
type=SearchItemType.ENTITY.value,
|
|
166
254
|
title=entity.title,
|
|
167
|
-
|
|
255
|
+
content_stems=entity_content_stems,
|
|
256
|
+
content_snippet=content_snippet,
|
|
168
257
|
permalink=entity.permalink,
|
|
169
258
|
file_path=entity.file_path,
|
|
259
|
+
entity_id=entity.id,
|
|
170
260
|
metadata={
|
|
171
261
|
"entity_type": entity.entity_type,
|
|
172
262
|
},
|
|
173
263
|
created_at=entity.created_at,
|
|
174
264
|
updated_at=entity.updated_at,
|
|
265
|
+
project_id=entity.project_id,
|
|
175
266
|
)
|
|
176
267
|
)
|
|
177
268
|
|
|
178
|
-
#
|
|
269
|
+
# Add observation rows
|
|
179
270
|
for obs in entity.observations:
|
|
180
271
|
# Index with parent entity's file path since that's where it's defined
|
|
181
|
-
|
|
272
|
+
obs_content_stems = "\n".join(
|
|
273
|
+
p for p in self._generate_variants(obs.content) if p and p.strip()
|
|
274
|
+
)
|
|
275
|
+
rows_to_index.append(
|
|
182
276
|
SearchIndexRow(
|
|
183
277
|
id=obs.id,
|
|
184
278
|
type=SearchItemType.OBSERVATION.value,
|
|
185
|
-
title=f"{obs.category}: {obs.content[:
|
|
186
|
-
|
|
279
|
+
title=f"{obs.category}: {obs.content[:100]}...",
|
|
280
|
+
content_stems=obs_content_stems,
|
|
281
|
+
content_snippet=obs.content,
|
|
187
282
|
permalink=obs.permalink,
|
|
188
283
|
file_path=entity.file_path,
|
|
189
284
|
category=obs.category,
|
|
@@ -193,10 +288,11 @@ class SearchService:
|
|
|
193
288
|
},
|
|
194
289
|
created_at=entity.created_at,
|
|
195
290
|
updated_at=entity.updated_at,
|
|
291
|
+
project_id=entity.project_id,
|
|
196
292
|
)
|
|
197
293
|
)
|
|
198
294
|
|
|
199
|
-
#
|
|
295
|
+
# Add relation rows (only outgoing relations defined in this file)
|
|
200
296
|
for rel in entity.outgoing_relations:
|
|
201
297
|
# Create descriptive title showing the relationship
|
|
202
298
|
relation_title = (
|
|
@@ -205,21 +301,63 @@ class SearchService:
|
|
|
205
301
|
else f"{rel.from_entity.title}"
|
|
206
302
|
)
|
|
207
303
|
|
|
208
|
-
|
|
304
|
+
rel_content_stems = "\n".join(
|
|
305
|
+
p for p in self._generate_variants(relation_title) if p and p.strip()
|
|
306
|
+
)
|
|
307
|
+
rows_to_index.append(
|
|
209
308
|
SearchIndexRow(
|
|
210
309
|
id=rel.id,
|
|
211
310
|
title=relation_title,
|
|
212
311
|
permalink=rel.permalink,
|
|
312
|
+
content_stems=rel_content_stems,
|
|
213
313
|
file_path=entity.file_path,
|
|
214
314
|
type=SearchItemType.RELATION.value,
|
|
315
|
+
entity_id=entity.id,
|
|
215
316
|
from_id=rel.from_id,
|
|
216
317
|
to_id=rel.to_id,
|
|
217
318
|
relation_type=rel.relation_type,
|
|
218
319
|
created_at=entity.created_at,
|
|
219
320
|
updated_at=entity.updated_at,
|
|
321
|
+
project_id=entity.project_id,
|
|
220
322
|
)
|
|
221
323
|
)
|
|
222
324
|
|
|
223
|
-
|
|
325
|
+
# Batch insert all rows at once
|
|
326
|
+
await self.repository.bulk_index_items(rows_to_index)
|
|
327
|
+
|
|
328
|
+
async def delete_by_permalink(self, permalink: str):
|
|
329
|
+
"""Delete an item from the search index."""
|
|
330
|
+
await self.repository.delete_by_permalink(permalink)
|
|
331
|
+
|
|
332
|
+
async def delete_by_entity_id(self, entity_id: int):
|
|
224
333
|
"""Delete an item from the search index."""
|
|
225
|
-
await self.repository.
|
|
334
|
+
await self.repository.delete_by_entity_id(entity_id)
|
|
335
|
+
|
|
336
|
+
async def handle_delete(self, entity: Entity):
|
|
337
|
+
"""Handle complete entity deletion from search index including observations and relations.
|
|
338
|
+
|
|
339
|
+
This replicates the logic from sync_service.handle_delete() to properly clean up
|
|
340
|
+
all search index entries for an entity and its related data.
|
|
341
|
+
"""
|
|
342
|
+
logger.debug(
|
|
343
|
+
f"Cleaning up search index for entity_id={entity.id}, file_path={entity.file_path}, "
|
|
344
|
+
f"observations={len(entity.observations)}, relations={len(entity.outgoing_relations)}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Clean up search index - same logic as sync_service.handle_delete()
|
|
348
|
+
permalinks = (
|
|
349
|
+
[entity.permalink]
|
|
350
|
+
+ [o.permalink for o in entity.observations]
|
|
351
|
+
+ [r.permalink for r in entity.outgoing_relations]
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
logger.debug(
|
|
355
|
+
f"Deleting search index entries for entity_id={entity.id}, "
|
|
356
|
+
f"index_entries={len(permalinks)}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
for permalink in permalinks:
|
|
360
|
+
if permalink:
|
|
361
|
+
await self.delete_by_permalink(permalink)
|
|
362
|
+
else:
|
|
363
|
+
await self.delete_by_entity_id(entity.id)
|
basic_memory/sync/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"""Basic Memory sync services."""
|
|
2
|
+
|
|
2
3
|
from .sync_service import SyncService
|
|
3
4
|
from .watch_service import WatchService
|
|
4
5
|
|
|
5
|
-
__all__ = ["SyncService", "
|
|
6
|
+
__all__ = ["SyncService", "WatchService"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from basic_memory.config import get_project_config
|
|
6
|
+
from basic_memory.sync import SyncService, WatchService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def sync_and_watch(
|
|
10
|
+
sync_service: SyncService, watch_service: WatchService
|
|
11
|
+
): # pragma: no cover
|
|
12
|
+
"""Run sync and watch service."""
|
|
13
|
+
|
|
14
|
+
config = get_project_config()
|
|
15
|
+
logger.info(f"Starting watch service to sync file changes in dir: {config.home}")
|
|
16
|
+
# full sync
|
|
17
|
+
await sync_service.sync(config.home)
|
|
18
|
+
|
|
19
|
+
# watch changes
|
|
20
|
+
await watch_service.run()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def create_background_sync_task(
|
|
24
|
+
sync_service: SyncService, watch_service: WatchService
|
|
25
|
+
): # pragma: no cover
|
|
26
|
+
return asyncio.create_task(sync_and_watch(sync_service, watch_service))
|