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
@@ -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 ProjectConfigDep, LinkResolverDep
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 permalink: {identifier}")
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 not entity:
26
- raise HTTPException(status_code=404, detail=f"Entity not found: {identifier}")
27
-
28
- file_path = Path(f"{config.home}/{entity.file_path}")
29
- if not file_path.exists():
30
- raise HTTPException(
31
- status_code=404,
32
- detail=f"File not found: {file_path}",
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
- return FileResponse(path=file_path)
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 dataclasses import asdict
3
+ from fastapi import APIRouter, BackgroundTasks
4
4
 
5
- from fastapi import APIRouter, Depends, BackgroundTasks
6
-
7
- from basic_memory.services.search_service import SearchService
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(query: SearchQuery, search_service: SearchService = Depends(get_search_service)):
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
- results = await search_service.search(query)
18
- search_results = [SearchResult.model_validate(asdict(r)) for r in results]
19
- return SearchResponse(results=search_results)
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