basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +145 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b1.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,260 @@
1
+ """Router for prompt-related operations.
2
+
3
+ This router is responsible for rendering various prompts using Handlebars templates.
4
+ It centralizes all prompt formatting logic that was previously in the MCP prompts.
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ from dateparser import parse
9
+ from fastapi import APIRouter, HTTPException, status
10
+ from loguru import logger
11
+
12
+ from basic_memory.api.routers.utils import to_graph_context, to_search_results
13
+ from basic_memory.api.template_loader import template_loader
14
+ from basic_memory.deps import (
15
+ ContextServiceDep,
16
+ EntityRepositoryDep,
17
+ SearchServiceDep,
18
+ EntityServiceDep,
19
+ )
20
+ from basic_memory.schemas.prompt import (
21
+ ContinueConversationRequest,
22
+ SearchPromptRequest,
23
+ PromptResponse,
24
+ PromptMetadata,
25
+ )
26
+ from basic_memory.schemas.search import SearchItemType, SearchQuery
27
+
28
+ router = APIRouter(prefix="/prompt", tags=["prompt"])
29
+
30
+
31
+ @router.post("/continue-conversation", response_model=PromptResponse)
32
+ async def continue_conversation(
33
+ search_service: SearchServiceDep,
34
+ entity_service: EntityServiceDep,
35
+ context_service: ContextServiceDep,
36
+ entity_repository: EntityRepositoryDep,
37
+ request: ContinueConversationRequest,
38
+ ) -> PromptResponse:
39
+ """Generate a prompt for continuing a conversation.
40
+
41
+ This endpoint takes a topic and/or timeframe and generates a prompt with
42
+ relevant context from the knowledge base.
43
+
44
+ Args:
45
+ request: The request parameters
46
+
47
+ Returns:
48
+ Formatted continuation prompt with context
49
+ """
50
+ logger.info(
51
+ f"Generating continue conversation prompt, topic: {request.topic}, timeframe: {request.timeframe}"
52
+ )
53
+
54
+ since = parse(request.timeframe) if request.timeframe else None
55
+
56
+ # Initialize search results
57
+ search_results = []
58
+
59
+ # Get data needed for template
60
+ if request.topic:
61
+ query = SearchQuery(text=request.topic, after_date=request.timeframe)
62
+ results = await search_service.search(query, limit=request.search_items_limit)
63
+ search_results = await to_search_results(entity_service, results)
64
+
65
+ # Build context from results
66
+ all_hierarchical_results = []
67
+ for result in search_results:
68
+ if hasattr(result, "permalink") and result.permalink:
69
+ # Get hierarchical context using the new dataclass-based approach
70
+ context_result = await context_service.build_context(
71
+ result.permalink,
72
+ depth=request.depth,
73
+ since=since,
74
+ max_related=request.related_items_limit,
75
+ include_observations=True, # Include observations for entities
76
+ )
77
+
78
+ # Process results into the schema format
79
+ graph_context = await to_graph_context(
80
+ context_result, entity_repository=entity_repository
81
+ )
82
+
83
+ # Add results to our collection (limit to top results for each permalink)
84
+ if graph_context.results:
85
+ all_hierarchical_results.extend(graph_context.results[:3])
86
+
87
+ # Limit to a reasonable number of total results
88
+ all_hierarchical_results = all_hierarchical_results[:10]
89
+
90
+ template_context = {
91
+ "topic": request.topic,
92
+ "timeframe": request.timeframe,
93
+ "hierarchical_results": all_hierarchical_results,
94
+ "has_results": len(all_hierarchical_results) > 0,
95
+ }
96
+ else:
97
+ # If no topic, get recent activity
98
+ context_result = await context_service.build_context(
99
+ types=[SearchItemType.ENTITY],
100
+ depth=request.depth,
101
+ since=since,
102
+ max_related=request.related_items_limit,
103
+ include_observations=True,
104
+ )
105
+ recent_context = await to_graph_context(context_result, entity_repository=entity_repository)
106
+
107
+ hierarchical_results = recent_context.results[:5] # Limit to top 5 recent items
108
+
109
+ template_context = {
110
+ "topic": f"Recent Activity from ({request.timeframe})",
111
+ "timeframe": request.timeframe,
112
+ "hierarchical_results": hierarchical_results,
113
+ "has_results": len(hierarchical_results) > 0,
114
+ }
115
+
116
+ try:
117
+ # Render template
118
+ rendered_prompt = await template_loader.render(
119
+ "prompts/continue_conversation.hbs", template_context
120
+ )
121
+
122
+ # Calculate metadata
123
+ # Count items of different types
124
+ observation_count = 0
125
+ relation_count = 0
126
+ entity_count = 0
127
+
128
+ # Get the hierarchical results from the template context
129
+ hierarchical_results_for_count = template_context.get("hierarchical_results", [])
130
+
131
+ # For topic-based search
132
+ if request.topic:
133
+ for item in hierarchical_results_for_count:
134
+ if hasattr(item, "observations"):
135
+ observation_count += len(item.observations) if item.observations else 0
136
+
137
+ if hasattr(item, "related_results"):
138
+ for related in item.related_results or []:
139
+ if hasattr(related, "type"):
140
+ if related.type == "relation":
141
+ relation_count += 1
142
+ elif related.type == "entity": # pragma: no cover
143
+ entity_count += 1 # pragma: no cover
144
+ # For recent activity
145
+ else:
146
+ for item in hierarchical_results_for_count:
147
+ if hasattr(item, "observations"):
148
+ observation_count += len(item.observations) if item.observations else 0
149
+
150
+ if hasattr(item, "related_results"):
151
+ for related in item.related_results or []:
152
+ if hasattr(related, "type"):
153
+ if related.type == "relation":
154
+ relation_count += 1
155
+ elif related.type == "entity": # pragma: no cover
156
+ entity_count += 1 # pragma: no cover
157
+
158
+ # Build metadata
159
+ metadata = {
160
+ "query": request.topic,
161
+ "timeframe": request.timeframe,
162
+ "search_count": len(search_results)
163
+ if request.topic
164
+ else 0, # Original search results count
165
+ "context_count": len(hierarchical_results_for_count),
166
+ "observation_count": observation_count,
167
+ "relation_count": relation_count,
168
+ "total_items": (
169
+ len(hierarchical_results_for_count)
170
+ + observation_count
171
+ + relation_count
172
+ + entity_count
173
+ ),
174
+ "search_limit": request.search_items_limit,
175
+ "context_depth": request.depth,
176
+ "related_limit": request.related_items_limit,
177
+ "generated_at": datetime.now(timezone.utc).isoformat(),
178
+ }
179
+
180
+ prompt_metadata = PromptMetadata(**metadata)
181
+
182
+ return PromptResponse(
183
+ prompt=rendered_prompt, context=template_context, metadata=prompt_metadata
184
+ )
185
+ except Exception as e:
186
+ logger.error(f"Error rendering continue conversation template: {e}")
187
+ raise HTTPException(
188
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
189
+ detail=f"Error rendering prompt template: {str(e)}",
190
+ )
191
+
192
+
193
+ @router.post("/search", response_model=PromptResponse)
194
+ async def search_prompt(
195
+ search_service: SearchServiceDep,
196
+ entity_service: EntityServiceDep,
197
+ request: SearchPromptRequest,
198
+ page: int = 1,
199
+ page_size: int = 10,
200
+ ) -> PromptResponse:
201
+ """Generate a prompt for search results.
202
+
203
+ This endpoint takes a search query and formats the results into a helpful
204
+ prompt with context and suggestions.
205
+
206
+ Args:
207
+ request: The search parameters
208
+ page: The page number for pagination
209
+ page_size: The number of results per page, defaults to 10
210
+
211
+ Returns:
212
+ Formatted search results prompt with context
213
+ """
214
+ logger.info(f"Generating search prompt, query: {request.query}, timeframe: {request.timeframe}")
215
+
216
+ limit = page_size
217
+ offset = (page - 1) * page_size
218
+
219
+ query = SearchQuery(text=request.query, after_date=request.timeframe)
220
+ results = await search_service.search(query, limit=limit, offset=offset)
221
+ search_results = await to_search_results(entity_service, results)
222
+
223
+ template_context = {
224
+ "query": request.query,
225
+ "timeframe": request.timeframe,
226
+ "results": search_results,
227
+ "has_results": len(search_results) > 0,
228
+ "result_count": len(search_results),
229
+ }
230
+
231
+ try:
232
+ # Render template
233
+ rendered_prompt = await template_loader.render("prompts/search.hbs", template_context)
234
+
235
+ # Build metadata
236
+ metadata = {
237
+ "query": request.query,
238
+ "timeframe": request.timeframe,
239
+ "search_count": len(search_results),
240
+ "context_count": len(search_results),
241
+ "observation_count": 0, # Search results don't include observations
242
+ "relation_count": 0, # Search results don't include relations
243
+ "total_items": len(search_results),
244
+ "search_limit": limit,
245
+ "context_depth": 0, # No context depth for basic search
246
+ "related_limit": 0, # No related items for basic search
247
+ "generated_at": datetime.now(timezone.utc).isoformat(),
248
+ }
249
+
250
+ prompt_metadata = PromptMetadata(**metadata)
251
+
252
+ return PromptResponse(
253
+ prompt=rendered_prompt, context=template_context, metadata=prompt_metadata
254
+ )
255
+ except Exception as e:
256
+ logger.error(f"Error rendering search template: {e}")
257
+ raise HTTPException(
258
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
259
+ detail=f"Error rendering prompt template: {str(e)}",
260
+ )
@@ -2,7 +2,8 @@
2
2
 
3
3
  from fastapi import APIRouter, BackgroundTasks
4
4
 
5
- from basic_memory.schemas.search import SearchQuery, SearchResult, SearchResponse
5
+ from basic_memory.api.routers.utils import to_search_results
6
+ from basic_memory.schemas.search import SearchQuery, SearchResponse
6
7
  from basic_memory.deps import SearchServiceDep, EntityServiceDep
7
8
 
8
9
  router = APIRouter(prefix="/search", tags=["search"])
@@ -20,26 +21,7 @@ async def search(
20
21
  limit = page_size
21
22
  offset = (page - 1) * page_size
22
23
  results = await search_service.search(query, limit=limit, offset=offset)
23
-
24
- search_results = []
25
- for r in results:
26
- entities = await entity_service.get_entities_by_id([r.entity_id, r.from_id, r.to_id]) # pyright: ignore
27
- search_results.append(
28
- SearchResult(
29
- title=r.title, # pyright: ignore
30
- type=r.type, # pyright: ignore
31
- permalink=r.permalink,
32
- score=r.score, # pyright: ignore
33
- entity=entities[0].permalink if entities else None,
34
- content=r.content,
35
- file_path=r.file_path,
36
- metadata=r.metadata,
37
- category=r.category,
38
- from_entity=entities[0].permalink if entities else None,
39
- to_entity=entities[1].permalink if len(entities) > 1 else None,
40
- relation_type=r.relation_type,
41
- )
42
- )
24
+ search_results = await to_search_results(entity_service, results)
43
25
  return SearchResponse(
44
26
  results=search_results,
45
27
  current_page=page,
@@ -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, # pyright: ignore
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