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,34 +1,239 @@
|
|
|
1
1
|
"""Routes for getting entity content."""
|
|
2
2
|
|
|
3
|
+
import tempfile
|
|
3
4
|
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
4
6
|
|
|
5
|
-
from fastapi import APIRouter, HTTPException
|
|
6
|
-
from fastapi.responses import FileResponse
|
|
7
|
+
from fastapi import APIRouter, HTTPException, BackgroundTasks, Body
|
|
8
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
7
9
|
from loguru import logger
|
|
8
10
|
|
|
9
|
-
from basic_memory.deps import
|
|
11
|
+
from basic_memory.deps import (
|
|
12
|
+
ProjectConfigDep,
|
|
13
|
+
LinkResolverDep,
|
|
14
|
+
SearchServiceDep,
|
|
15
|
+
EntityServiceDep,
|
|
16
|
+
FileServiceDep,
|
|
17
|
+
EntityRepositoryDep,
|
|
18
|
+
)
|
|
19
|
+
from basic_memory.repository.search_repository import SearchIndexRow
|
|
20
|
+
from basic_memory.schemas.memory import normalize_memory_url
|
|
21
|
+
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
|
22
|
+
from basic_memory.models.knowledge import Entity as EntityModel
|
|
23
|
+
from datetime import datetime
|
|
10
24
|
|
|
11
25
|
router = APIRouter(prefix="/resource", tags=["resources"])
|
|
12
26
|
|
|
13
27
|
|
|
28
|
+
def get_entity_ids(item: SearchIndexRow) -> set[int]:
|
|
29
|
+
match item.type:
|
|
30
|
+
case SearchItemType.ENTITY:
|
|
31
|
+
return {item.id}
|
|
32
|
+
case SearchItemType.OBSERVATION:
|
|
33
|
+
return {item.entity_id} # pyright: ignore [reportReturnType]
|
|
34
|
+
case SearchItemType.RELATION:
|
|
35
|
+
from_entity = item.from_id
|
|
36
|
+
to_entity = item.to_id # pyright: ignore [reportReturnType]
|
|
37
|
+
return {from_entity, to_entity} if to_entity else {from_entity} # pyright: ignore [reportReturnType]
|
|
38
|
+
case _: # pragma: no cover
|
|
39
|
+
raise ValueError(f"Unexpected type: {item.type}")
|
|
40
|
+
|
|
41
|
+
|
|
14
42
|
@router.get("/{identifier:path}")
|
|
15
43
|
async def get_resource_content(
|
|
16
44
|
config: ProjectConfigDep,
|
|
17
45
|
link_resolver: LinkResolverDep,
|
|
46
|
+
search_service: SearchServiceDep,
|
|
47
|
+
entity_service: EntityServiceDep,
|
|
48
|
+
file_service: FileServiceDep,
|
|
49
|
+
background_tasks: BackgroundTasks,
|
|
18
50
|
identifier: str,
|
|
51
|
+
page: int = 1,
|
|
52
|
+
page_size: int = 10,
|
|
19
53
|
) -> FileResponse:
|
|
20
54
|
"""Get resource content by identifier: name or permalink."""
|
|
21
|
-
logger.debug(f"Getting content for
|
|
55
|
+
logger.debug(f"Getting content for: {identifier}")
|
|
22
56
|
|
|
23
|
-
# Find entity by permalink
|
|
57
|
+
# Find single entity by permalink
|
|
24
58
|
entity = await link_resolver.resolve_link(identifier)
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
results = [entity] if entity else []
|
|
60
|
+
|
|
61
|
+
# pagination for multiple results
|
|
62
|
+
limit = page_size
|
|
63
|
+
offset = (page - 1) * page_size
|
|
64
|
+
|
|
65
|
+
# search using the identifier as a permalink
|
|
66
|
+
if not results:
|
|
67
|
+
# if the identifier contains a wildcard, use GLOB search
|
|
68
|
+
query = (
|
|
69
|
+
SearchQuery(permalink_match=identifier)
|
|
70
|
+
if "*" in identifier
|
|
71
|
+
else SearchQuery(permalink=identifier)
|
|
72
|
+
)
|
|
73
|
+
search_results = await search_service.search(query, limit, offset)
|
|
74
|
+
if not search_results:
|
|
75
|
+
raise HTTPException(status_code=404, detail=f"Resource not found: {identifier}")
|
|
76
|
+
|
|
77
|
+
# get the deduplicated entities related to the search results
|
|
78
|
+
entity_ids = {id for result in search_results for id in get_entity_ids(result)}
|
|
79
|
+
results = await entity_service.get_entities_by_id(list(entity_ids))
|
|
80
|
+
|
|
81
|
+
# return single response
|
|
82
|
+
if len(results) == 1:
|
|
83
|
+
entity = results[0]
|
|
84
|
+
file_path = Path(f"{config.home}/{entity.file_path}")
|
|
85
|
+
if not file_path.exists():
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=404,
|
|
88
|
+
detail=f"File not found: {file_path}",
|
|
89
|
+
)
|
|
90
|
+
return FileResponse(path=file_path)
|
|
91
|
+
|
|
92
|
+
# for multiple files, initialize a temporary file for writing the results
|
|
93
|
+
with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".md") as tmp_file:
|
|
94
|
+
temp_file_path = tmp_file.name
|
|
95
|
+
|
|
96
|
+
for result in results:
|
|
97
|
+
# Read content for each entity
|
|
98
|
+
content = await file_service.read_entity_content(result)
|
|
99
|
+
memory_url = normalize_memory_url(result.permalink)
|
|
100
|
+
modified_date = result.updated_at.isoformat()
|
|
101
|
+
checksum = result.checksum[:8] if result.checksum else ""
|
|
102
|
+
|
|
103
|
+
# Prepare the delimited content
|
|
104
|
+
response_content = f"--- {memory_url} {modified_date} {checksum}\n"
|
|
105
|
+
response_content += f"\n{content}\n"
|
|
106
|
+
response_content += "\n"
|
|
107
|
+
|
|
108
|
+
# Write content directly to the temporary file in append mode
|
|
109
|
+
tmp_file.write(response_content)
|
|
110
|
+
|
|
111
|
+
# Ensure all content is written to disk
|
|
112
|
+
tmp_file.flush()
|
|
113
|
+
|
|
114
|
+
# Schedule the temporary file to be deleted after the response
|
|
115
|
+
background_tasks.add_task(cleanup_temp_file, temp_file_path)
|
|
116
|
+
|
|
117
|
+
# Return the file response
|
|
118
|
+
return FileResponse(path=temp_file_path)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cleanup_temp_file(file_path: str):
|
|
122
|
+
"""Delete the temporary file."""
|
|
123
|
+
try:
|
|
124
|
+
Path(file_path).unlink() # Deletes the file
|
|
125
|
+
logger.debug(f"Temporary file deleted: {file_path}")
|
|
126
|
+
except Exception as e: # pragma: no cover
|
|
127
|
+
logger.error(f"Error deleting temporary file {file_path}: {e}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@router.put("/{file_path:path}")
|
|
131
|
+
async def write_resource(
|
|
132
|
+
config: ProjectConfigDep,
|
|
133
|
+
file_service: FileServiceDep,
|
|
134
|
+
entity_repository: EntityRepositoryDep,
|
|
135
|
+
search_service: SearchServiceDep,
|
|
136
|
+
file_path: str,
|
|
137
|
+
content: Annotated[str, Body()],
|
|
138
|
+
) -> JSONResponse:
|
|
139
|
+
"""Write content to a file in the project.
|
|
140
|
+
|
|
141
|
+
This endpoint allows writing content directly to a file in the project.
|
|
142
|
+
Also creates an entity record and indexes the file for search.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
file_path: Path to write to, relative to project root
|
|
146
|
+
request: Contains the content to write
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
JSON response with file information
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
# Get content from request body
|
|
153
|
+
|
|
154
|
+
# Defensive type checking: ensure content is a string
|
|
155
|
+
# FastAPI should validate this, but if a dict somehow gets through
|
|
156
|
+
# (e.g., via JSON body parsing), we need to catch it here
|
|
157
|
+
if isinstance(content, dict):
|
|
158
|
+
logger.error(
|
|
159
|
+
f"Error writing resource {file_path}: "
|
|
160
|
+
f"content is a dict, expected string. Keys: {list(content.keys())}"
|
|
161
|
+
)
|
|
162
|
+
raise HTTPException(
|
|
163
|
+
status_code=400,
|
|
164
|
+
detail="content must be a string, not a dict. "
|
|
165
|
+
"Ensure request body is sent as raw string content, not JSON object.",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Ensure it's UTF-8 string content
|
|
169
|
+
if isinstance(content, bytes): # pragma: no cover
|
|
170
|
+
content_str = content.decode("utf-8")
|
|
171
|
+
else:
|
|
172
|
+
content_str = str(content)
|
|
173
|
+
|
|
174
|
+
# Get full file path
|
|
175
|
+
full_path = Path(f"{config.home}/{file_path}")
|
|
176
|
+
|
|
177
|
+
# Ensure parent directory exists
|
|
178
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
# Write content to file
|
|
181
|
+
checksum = await file_service.write_file(full_path, content_str)
|
|
182
|
+
|
|
183
|
+
# Get file info
|
|
184
|
+
file_stats = file_service.file_stats(full_path)
|
|
185
|
+
|
|
186
|
+
# Determine file details
|
|
187
|
+
file_name = Path(file_path).name
|
|
188
|
+
content_type = file_service.content_type(full_path)
|
|
189
|
+
|
|
190
|
+
entity_type = "canvas" if file_path.endswith(".canvas") else "file"
|
|
191
|
+
|
|
192
|
+
# Check if entity already exists
|
|
193
|
+
existing_entity = await entity_repository.get_by_file_path(file_path)
|
|
194
|
+
|
|
195
|
+
if existing_entity:
|
|
196
|
+
# Update existing entity
|
|
197
|
+
entity = await entity_repository.update(
|
|
198
|
+
existing_entity.id,
|
|
199
|
+
{
|
|
200
|
+
"title": file_name,
|
|
201
|
+
"entity_type": entity_type,
|
|
202
|
+
"content_type": content_type,
|
|
203
|
+
"file_path": file_path,
|
|
204
|
+
"checksum": checksum,
|
|
205
|
+
"updated_at": datetime.fromtimestamp(file_stats.st_mtime).astimezone(),
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
status_code = 200
|
|
209
|
+
else:
|
|
210
|
+
# Create a new entity model
|
|
211
|
+
entity = EntityModel(
|
|
212
|
+
title=file_name,
|
|
213
|
+
entity_type=entity_type,
|
|
214
|
+
content_type=content_type,
|
|
215
|
+
file_path=file_path,
|
|
216
|
+
checksum=checksum,
|
|
217
|
+
created_at=datetime.fromtimestamp(file_stats.st_ctime).astimezone(),
|
|
218
|
+
updated_at=datetime.fromtimestamp(file_stats.st_mtime).astimezone(),
|
|
219
|
+
)
|
|
220
|
+
entity = await entity_repository.add(entity)
|
|
221
|
+
status_code = 201
|
|
222
|
+
|
|
223
|
+
# Index the file for search
|
|
224
|
+
await search_service.index_entity(entity) # pyright: ignore
|
|
225
|
+
|
|
226
|
+
# Return success response
|
|
227
|
+
return JSONResponse(
|
|
228
|
+
status_code=status_code,
|
|
229
|
+
content={
|
|
230
|
+
"file_path": file_path,
|
|
231
|
+
"checksum": checksum,
|
|
232
|
+
"size": file_stats.st_size,
|
|
233
|
+
"created_at": file_stats.st_ctime,
|
|
234
|
+
"modified_at": file_stats.st_mtime,
|
|
235
|
+
},
|
|
33
236
|
)
|
|
34
|
-
|
|
237
|
+
except Exception as e: # pragma: no cover
|
|
238
|
+
logger.error(f"Error writing resource {file_path}: {e}")
|
|
239
|
+
raise HTTPException(status_code=500, detail=f"Failed to write resource: {str(e)}")
|
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
"""Router for search operations."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from fastapi import APIRouter, BackgroundTasks
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from basic_memory.
|
|
8
|
-
from basic_memory.schemas.search import SearchQuery, SearchResult, SearchResponse
|
|
9
|
-
from basic_memory.deps import get_search_service
|
|
5
|
+
from basic_memory.api.routers.utils import to_search_results
|
|
6
|
+
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
|
7
|
+
from basic_memory.deps import SearchServiceDep, EntityServiceDep
|
|
10
8
|
|
|
11
9
|
router = APIRouter(prefix="/search", tags=["search"])
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
@router.post("/", response_model=SearchResponse)
|
|
15
|
-
async def search(
|
|
13
|
+
async def search(
|
|
14
|
+
query: SearchQuery,
|
|
15
|
+
search_service: SearchServiceDep,
|
|
16
|
+
entity_service: EntityServiceDep,
|
|
17
|
+
page: int = 1,
|
|
18
|
+
page_size: int = 10,
|
|
19
|
+
):
|
|
16
20
|
"""Search across all knowledge and documents."""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
limit = page_size
|
|
22
|
+
offset = (page - 1) * page_size
|
|
23
|
+
results = await search_service.search(query, limit=limit, offset=offset)
|
|
24
|
+
search_results = await to_search_results(entity_service, results)
|
|
25
|
+
return SearchResponse(
|
|
26
|
+
results=search_results,
|
|
27
|
+
current_page=page,
|
|
28
|
+
page_size=page_size,
|
|
29
|
+
)
|
|
20
30
|
|
|
21
31
|
|
|
22
32
|
@router.post("/reindex")
|
|
23
|
-
async def reindex(
|
|
24
|
-
background_tasks: BackgroundTasks, search_service: SearchService = Depends(get_search_service)
|
|
25
|
-
):
|
|
33
|
+
async def reindex(background_tasks: BackgroundTasks, search_service: SearchServiceDep):
|
|
26
34
|
"""Recreate and populate the search index."""
|
|
27
35
|
await search_service.reindex_all(background_tasks=background_tasks)
|
|
28
36
|
return {"status": "ok", "message": "Reindex initiated"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
|
|
3
|
+
from basic_memory.repository import EntityRepository
|
|
4
|
+
from basic_memory.repository.search_repository import SearchIndexRow
|
|
5
|
+
from basic_memory.schemas.memory import (
|
|
6
|
+
EntitySummary,
|
|
7
|
+
ObservationSummary,
|
|
8
|
+
RelationSummary,
|
|
9
|
+
MemoryMetadata,
|
|
10
|
+
GraphContext,
|
|
11
|
+
ContextResult,
|
|
12
|
+
)
|
|
13
|
+
from basic_memory.schemas.search import SearchItemType, SearchResult
|
|
14
|
+
from basic_memory.services import EntityService
|
|
15
|
+
from basic_memory.services.context_service import (
|
|
16
|
+
ContextResultRow,
|
|
17
|
+
ContextResult as ServiceContextResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def to_graph_context(
|
|
22
|
+
context_result: ServiceContextResult,
|
|
23
|
+
entity_repository: EntityRepository,
|
|
24
|
+
page: Optional[int] = None,
|
|
25
|
+
page_size: Optional[int] = None,
|
|
26
|
+
):
|
|
27
|
+
# Helper function to convert items to summaries
|
|
28
|
+
async def to_summary(item: SearchIndexRow | ContextResultRow):
|
|
29
|
+
match item.type:
|
|
30
|
+
case SearchItemType.ENTITY:
|
|
31
|
+
return EntitySummary(
|
|
32
|
+
title=item.title, # pyright: ignore
|
|
33
|
+
permalink=item.permalink,
|
|
34
|
+
content=item.content,
|
|
35
|
+
file_path=item.file_path,
|
|
36
|
+
created_at=item.created_at,
|
|
37
|
+
)
|
|
38
|
+
case SearchItemType.OBSERVATION:
|
|
39
|
+
return ObservationSummary(
|
|
40
|
+
title=item.title, # pyright: ignore
|
|
41
|
+
file_path=item.file_path,
|
|
42
|
+
category=item.category, # pyright: ignore
|
|
43
|
+
content=item.content, # pyright: ignore
|
|
44
|
+
permalink=item.permalink, # pyright: ignore
|
|
45
|
+
created_at=item.created_at,
|
|
46
|
+
)
|
|
47
|
+
case SearchItemType.RELATION:
|
|
48
|
+
from_entity = await entity_repository.find_by_id(item.from_id) # pyright: ignore
|
|
49
|
+
to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
|
|
50
|
+
return RelationSummary(
|
|
51
|
+
title=item.title, # pyright: ignore
|
|
52
|
+
file_path=item.file_path,
|
|
53
|
+
permalink=item.permalink, # pyright: ignore
|
|
54
|
+
relation_type=item.relation_type, # pyright: ignore
|
|
55
|
+
from_entity=from_entity.title if from_entity else None,
|
|
56
|
+
to_entity=to_entity.title if to_entity else None,
|
|
57
|
+
created_at=item.created_at,
|
|
58
|
+
)
|
|
59
|
+
case _: # pragma: no cover
|
|
60
|
+
raise ValueError(f"Unexpected type: {item.type}")
|
|
61
|
+
|
|
62
|
+
# Process the hierarchical results
|
|
63
|
+
hierarchical_results = []
|
|
64
|
+
for context_item in context_result.results:
|
|
65
|
+
# Process primary result
|
|
66
|
+
primary_result = await to_summary(context_item.primary_result)
|
|
67
|
+
|
|
68
|
+
# Process observations
|
|
69
|
+
observations = []
|
|
70
|
+
for obs in context_item.observations:
|
|
71
|
+
observations.append(await to_summary(obs))
|
|
72
|
+
|
|
73
|
+
# Process related results
|
|
74
|
+
related = []
|
|
75
|
+
for rel in context_item.related_results:
|
|
76
|
+
related.append(await to_summary(rel))
|
|
77
|
+
|
|
78
|
+
# Add to hierarchical results
|
|
79
|
+
hierarchical_results.append(
|
|
80
|
+
ContextResult(
|
|
81
|
+
primary_result=primary_result,
|
|
82
|
+
observations=observations,
|
|
83
|
+
related_results=related,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Create schema metadata from service metadata
|
|
88
|
+
metadata = MemoryMetadata(
|
|
89
|
+
uri=context_result.metadata.uri,
|
|
90
|
+
types=context_result.metadata.types,
|
|
91
|
+
depth=context_result.metadata.depth,
|
|
92
|
+
timeframe=context_result.metadata.timeframe,
|
|
93
|
+
generated_at=context_result.metadata.generated_at,
|
|
94
|
+
primary_count=context_result.metadata.primary_count,
|
|
95
|
+
related_count=context_result.metadata.related_count,
|
|
96
|
+
total_results=context_result.metadata.primary_count + context_result.metadata.related_count,
|
|
97
|
+
total_relations=context_result.metadata.total_relations,
|
|
98
|
+
total_observations=context_result.metadata.total_observations,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Return new GraphContext with just hierarchical results
|
|
102
|
+
return GraphContext(
|
|
103
|
+
results=hierarchical_results,
|
|
104
|
+
metadata=metadata,
|
|
105
|
+
page=page,
|
|
106
|
+
page_size=page_size,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def to_search_results(entity_service: EntityService, results: List[SearchIndexRow]):
|
|
111
|
+
search_results = []
|
|
112
|
+
for r in results:
|
|
113
|
+
entities = await entity_service.get_entities_by_id([r.entity_id, r.from_id, r.to_id]) # pyright: ignore
|
|
114
|
+
search_results.append(
|
|
115
|
+
SearchResult(
|
|
116
|
+
title=r.title, # pyright: ignore
|
|
117
|
+
type=r.type, # pyright: ignore
|
|
118
|
+
permalink=r.permalink,
|
|
119
|
+
score=r.score, # pyright: ignore
|
|
120
|
+
entity=entities[0].permalink if entities else None,
|
|
121
|
+
content=r.content,
|
|
122
|
+
file_path=r.file_path,
|
|
123
|
+
metadata=r.metadata,
|
|
124
|
+
category=r.category,
|
|
125
|
+
from_entity=entities[0].permalink if entities else None,
|
|
126
|
+
to_entity=entities[1].permalink if len(entities) > 1 else None,
|
|
127
|
+
relation_type=r.relation_type,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return search_results
|