basic-memory 0.12.2__py3-none-any.whl → 0.13.0__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 +2 -1
- basic_memory/alembic/env.py +1 -1
- 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/cc7172b46608_update_search_index_schema.py +0 -6
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +139 -37
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +6 -62
- basic_memory/api/routers/project_router.py +234 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +102 -70
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/commands/tool.py +6 -6
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +143 -87
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +20 -4
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/sync_status.py +116 -0
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +86 -13
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +24 -0
- basic_memory/mcp/tools/build_context.py +43 -8
- basic_memory/mcp/tools/canvas.py +17 -3
- basic_memory/mcp/tools/delete_note.py +168 -5
- basic_memory/mcp/tools/edit_note.py +303 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +299 -0
- basic_memory/mcp/tools/project_management.py +332 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +28 -9
- basic_memory/mcp/tools/recent_activity.py +47 -16
- basic_memory/mcp/tools/search.py +189 -8
- basic_memory/mcp/tools/sync_status.py +254 -0
- basic_memory/mcp/tools/utils.py +184 -12
- basic_memory/mcp/tools/view_note.py +66 -0
- basic_memory/mcp/tools/write_note.py +24 -17
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +78 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +192 -54
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +33 -5
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +84 -13
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +399 -6
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +170 -66
- basic_memory/services/link_resolver.py +35 -12
- basic_memory/services/migration_service.py +168 -0
- basic_memory/services/project_service.py +671 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/services/sync_status_service.py +181 -0
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +102 -21
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +67 -17
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/METADATA +26 -4
- basic_memory-0.13.0.dist-info/RECORD +138 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.2.dist-info/RECORD +0 -100
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,10 +10,12 @@ from basic_memory.mcp.prompts import continue_conversation
|
|
|
10
10
|
from basic_memory.mcp.prompts import recent_activity
|
|
11
11
|
from basic_memory.mcp.prompts import search
|
|
12
12
|
from basic_memory.mcp.prompts import ai_assistant_guide
|
|
13
|
+
from basic_memory.mcp.prompts import sync_status
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"ai_assistant_guide",
|
|
16
17
|
"continue_conversation",
|
|
17
18
|
"recent_activity",
|
|
18
19
|
"search",
|
|
20
|
+
"sync_status",
|
|
19
21
|
]
|
|
@@ -4,20 +4,17 @@ These prompts help users continue conversations and work across sessions,
|
|
|
4
4
|
providing context from previous interactions to maintain continuity.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from textwrap import dedent
|
|
8
7
|
from typing import Annotated, Optional
|
|
9
8
|
|
|
10
9
|
from loguru import logger
|
|
11
10
|
from pydantic import Field
|
|
12
11
|
|
|
13
|
-
from basic_memory.
|
|
12
|
+
from basic_memory.config import get_project_config
|
|
13
|
+
from basic_memory.mcp.async_client import client
|
|
14
14
|
from basic_memory.mcp.server import mcp
|
|
15
|
-
from basic_memory.mcp.tools.
|
|
16
|
-
from basic_memory.mcp.tools.recent_activity import recent_activity
|
|
17
|
-
from basic_memory.mcp.tools.search import search_notes
|
|
15
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
18
16
|
from basic_memory.schemas.base import TimeFrame
|
|
19
|
-
from basic_memory.schemas.
|
|
20
|
-
from basic_memory.schemas.search import SearchItemType
|
|
17
|
+
from basic_memory.schemas.prompt import ContinueConversationRequest
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
@mcp.prompt(
|
|
@@ -45,67 +42,20 @@ async def continue_conversation(
|
|
|
45
42
|
"""
|
|
46
43
|
logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
|
|
47
44
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
45
|
+
# Create request model
|
|
46
|
+
request = ContinueConversationRequest( # pyright: ignore [reportCallIssue]
|
|
47
|
+
topic=topic, timeframe=timeframe
|
|
48
|
+
)
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
contexts = []
|
|
56
|
-
for result in search_results.results:
|
|
57
|
-
if hasattr(result, "permalink") and result.permalink:
|
|
58
|
-
context: GraphContext = await build_context(f"memory://{result.permalink}")
|
|
59
|
-
if context.primary_results:
|
|
60
|
-
contexts.append(
|
|
61
|
-
PromptContextItem(
|
|
62
|
-
primary_results=context.primary_results[:1], # pyright: ignore
|
|
63
|
-
related_results=context.related_results[:3], # pyright: ignore
|
|
64
|
-
)
|
|
65
|
-
)
|
|
50
|
+
project_url = get_project_config().project_url
|
|
66
51
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
# Call the prompt API endpoint
|
|
53
|
+
response = await call_post(
|
|
54
|
+
client,
|
|
55
|
+
f"{project_url}/prompt/continue-conversation",
|
|
56
|
+
json=request.model_dump(exclude_none=True),
|
|
57
|
+
)
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
recent: GraphContext = await recent_activity(
|
|
76
|
-
timeframe=timeframe, type=[SearchItemType.ENTITY]
|
|
77
|
-
)
|
|
78
|
-
prompt_context = format_prompt_context(
|
|
79
|
-
PromptContext(
|
|
80
|
-
topic=f"Recent Activity from ({timeframe})",
|
|
81
|
-
timeframe=timeframe,
|
|
82
|
-
results=[
|
|
83
|
-
PromptContextItem(
|
|
84
|
-
primary_results=recent.primary_results[:5], # pyright: ignore
|
|
85
|
-
related_results=recent.related_results[:2], # pyright: ignore
|
|
86
|
-
)
|
|
87
|
-
],
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# Add next steps with strong encouragement to write
|
|
92
|
-
next_steps = dedent(f"""
|
|
93
|
-
## Next Steps
|
|
94
|
-
|
|
95
|
-
You can:
|
|
96
|
-
- Explore more with: `search_notes({{"text": "{topic}"}})`
|
|
97
|
-
- See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
|
|
98
|
-
- **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
|
|
99
|
-
|
|
100
|
-
## Knowledge Capture Recommendation
|
|
101
|
-
|
|
102
|
-
As you continue this conversation, **actively look for opportunities to:**
|
|
103
|
-
1. Record key information, decisions, or insights that emerge
|
|
104
|
-
2. Link new knowledge to existing topics
|
|
105
|
-
3. Suggest capturing important context when appropriate
|
|
106
|
-
4. Create forward references to topics that might be created later
|
|
107
|
-
|
|
108
|
-
Remember that capturing knowledge during conversations is one of the most valuable aspects of Basic Memory.
|
|
109
|
-
""")
|
|
110
|
-
|
|
111
|
-
return prompt_context + next_steps
|
|
59
|
+
# Extract the rendered prompt from the response
|
|
60
|
+
result = response.json()
|
|
61
|
+
return result["prompt"]
|
|
@@ -38,7 +38,19 @@ async def recent_activity_prompt(
|
|
|
38
38
|
"""
|
|
39
39
|
logger.info(f"Getting recent activity, timeframe: {timeframe}")
|
|
40
40
|
|
|
41
|
-
recent = await recent_activity(timeframe=timeframe, type=[SearchItemType.ENTITY])
|
|
41
|
+
recent = await recent_activity.fn(timeframe=timeframe, type=[SearchItemType.ENTITY])
|
|
42
|
+
|
|
43
|
+
# Extract primary results from the hierarchical structure
|
|
44
|
+
primary_results = []
|
|
45
|
+
related_results = []
|
|
46
|
+
|
|
47
|
+
if recent.results:
|
|
48
|
+
# Take up to 5 primary results
|
|
49
|
+
for item in recent.results[:5]:
|
|
50
|
+
primary_results.append(item.primary_result)
|
|
51
|
+
# Add up to 2 related results per primary item
|
|
52
|
+
if item.related_results:
|
|
53
|
+
related_results.extend(item.related_results[:2])
|
|
42
54
|
|
|
43
55
|
prompt_context = format_prompt_context(
|
|
44
56
|
PromptContext(
|
|
@@ -46,14 +58,18 @@ async def recent_activity_prompt(
|
|
|
46
58
|
timeframe=timeframe,
|
|
47
59
|
results=[
|
|
48
60
|
PromptContextItem(
|
|
49
|
-
primary_results=
|
|
50
|
-
related_results=
|
|
61
|
+
primary_results=primary_results,
|
|
62
|
+
related_results=related_results[:10], # Limit total related results
|
|
51
63
|
)
|
|
52
64
|
],
|
|
53
65
|
)
|
|
54
66
|
)
|
|
55
67
|
|
|
56
68
|
# Add suggestions for summarizing recent activity
|
|
69
|
+
first_title = "Recent Topic"
|
|
70
|
+
if primary_results and len(primary_results) > 0:
|
|
71
|
+
first_title = primary_results[0].title
|
|
72
|
+
|
|
57
73
|
capture_suggestions = f"""
|
|
58
74
|
## Opportunity to Capture Activity Summary
|
|
59
75
|
|
|
@@ -76,7 +92,7 @@ async def recent_activity_prompt(
|
|
|
76
92
|
- [insight] [Connection between different activities]
|
|
77
93
|
|
|
78
94
|
## Relations
|
|
79
|
-
- summarizes [[{
|
|
95
|
+
- summarizes [[{first_title}]]
|
|
80
96
|
- relates_to [[Project Overview]]
|
|
81
97
|
'''
|
|
82
98
|
)
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
These prompts help users search and explore their knowledge base.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from textwrap import dedent
|
|
7
6
|
from typing import Annotated, Optional
|
|
8
7
|
|
|
9
8
|
from loguru import logger
|
|
10
9
|
from pydantic import Field
|
|
11
10
|
|
|
11
|
+
from basic_memory.config import get_project_config
|
|
12
|
+
from basic_memory.mcp.async_client import client
|
|
12
13
|
from basic_memory.mcp.server import mcp
|
|
13
|
-
from basic_memory.mcp.tools.
|
|
14
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
14
15
|
from basic_memory.schemas.base import TimeFrame
|
|
15
|
-
from basic_memory.schemas.
|
|
16
|
+
from basic_memory.schemas.prompt import SearchPromptRequest
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@mcp.prompt(
|
|
@@ -40,143 +41,16 @@ async def search_prompt(
|
|
|
40
41
|
"""
|
|
41
42
|
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
# Create request model
|
|
45
|
+
request = SearchPromptRequest(query=query, timeframe=timeframe)
|
|
45
46
|
|
|
47
|
+
project_url = get_project_config().project_url
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
query: The search query
|
|
54
|
-
results: Search results object
|
|
55
|
-
timeframe: How far back results were searched
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Formatted search results summary
|
|
59
|
-
"""
|
|
60
|
-
if not results.results:
|
|
61
|
-
return dedent(f"""
|
|
62
|
-
# Search Results for: "{query}"
|
|
63
|
-
|
|
64
|
-
I couldn't find any results for this query.
|
|
65
|
-
|
|
66
|
-
## Opportunity to Capture Knowledge!
|
|
67
|
-
|
|
68
|
-
This is an excellent opportunity to create new knowledge on this topic. Consider:
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
await write_note(
|
|
72
|
-
title="{query.capitalize()}",
|
|
73
|
-
content=f'''
|
|
74
|
-
# {query.capitalize()}
|
|
75
|
-
|
|
76
|
-
## Overview
|
|
77
|
-
[Summary of what we've discussed about {query}]
|
|
78
|
-
|
|
79
|
-
## Observations
|
|
80
|
-
- [category] [First observation about {query}]
|
|
81
|
-
- [category] [Second observation about {query}]
|
|
82
|
-
|
|
83
|
-
## Relations
|
|
84
|
-
- relates_to [[Other Relevant Topic]]
|
|
85
|
-
'''
|
|
86
|
-
)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Other Suggestions
|
|
90
|
-
- Try a different search term
|
|
91
|
-
- Broaden your search criteria
|
|
92
|
-
- Check recent activity with `recent_activity(timeframe="1w")`
|
|
93
|
-
""")
|
|
94
|
-
|
|
95
|
-
# Start building our summary with header
|
|
96
|
-
time_info = f" (after {timeframe})" if timeframe else ""
|
|
97
|
-
summary = dedent(f"""
|
|
98
|
-
# Search Results for: "{query}"{time_info}
|
|
99
|
-
|
|
100
|
-
This is a memory search session.
|
|
101
|
-
Please use the available basic-memory tools to gather relevant context before responding.
|
|
102
|
-
I found {len(results.results)} results that match your query.
|
|
103
|
-
|
|
104
|
-
Here are the most relevant results:
|
|
105
|
-
""")
|
|
106
|
-
|
|
107
|
-
# Add each search result
|
|
108
|
-
for i, result in enumerate(results.results[:5]): # Limit to top 5 results
|
|
109
|
-
summary += dedent(f"""
|
|
110
|
-
## {i + 1}. {result.title}
|
|
111
|
-
- **Type**: {result.type.value}
|
|
112
|
-
""")
|
|
113
|
-
|
|
114
|
-
# Add creation date if available in metadata
|
|
115
|
-
if result.metadata and "created_at" in result.metadata:
|
|
116
|
-
created_at = result.metadata["created_at"]
|
|
117
|
-
if hasattr(created_at, "strftime"):
|
|
118
|
-
summary += (
|
|
119
|
-
f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n" # pragma: no cover
|
|
120
|
-
)
|
|
121
|
-
elif isinstance(created_at, str):
|
|
122
|
-
summary += f"- **Created**: {created_at}\n"
|
|
123
|
-
|
|
124
|
-
# Add score and excerpt
|
|
125
|
-
summary += f"- **Relevance Score**: {result.score:.2f}\n"
|
|
126
|
-
|
|
127
|
-
# Add excerpt if available in metadata
|
|
128
|
-
if result.content:
|
|
129
|
-
summary += f"- **Excerpt**:\n{result.content}\n"
|
|
130
|
-
|
|
131
|
-
# Add permalink for retrieving content
|
|
132
|
-
if result.permalink:
|
|
133
|
-
summary += dedent(f"""
|
|
134
|
-
You can view this content with: `read_note("{result.permalink}")`
|
|
135
|
-
Or explore its context with: `build_context("memory://{result.permalink}")`
|
|
136
|
-
""")
|
|
137
|
-
else:
|
|
138
|
-
summary += dedent(f"""
|
|
139
|
-
You can view this file with: `read_file("{result.file_path}")`
|
|
140
|
-
""") # pragma: no cover
|
|
141
|
-
|
|
142
|
-
# Add next steps with strong write encouragement
|
|
143
|
-
summary += dedent(f"""
|
|
144
|
-
## Next Steps
|
|
145
|
-
|
|
146
|
-
You can:
|
|
147
|
-
- Refine your search: `search_notes("{query} AND additional_term")`
|
|
148
|
-
- Exclude terms: `search_notes("{query} NOT exclude_term")`
|
|
149
|
-
- View more results: `search_notes("{query}", after_date=None)`
|
|
150
|
-
- Check recent activity: `recent_activity()`
|
|
151
|
-
|
|
152
|
-
## Synthesize and Capture Knowledge
|
|
153
|
-
|
|
154
|
-
Consider creating a new note that synthesizes what you've learned:
|
|
155
|
-
|
|
156
|
-
```python
|
|
157
|
-
await write_note(
|
|
158
|
-
title="Synthesis of {query.capitalize()} Information",
|
|
159
|
-
content='''
|
|
160
|
-
# Synthesis of {query.capitalize()} Information
|
|
161
|
-
|
|
162
|
-
## Overview
|
|
163
|
-
[Synthesis of the search results and your conversation]
|
|
164
|
-
|
|
165
|
-
## Key Insights
|
|
166
|
-
[Summary of main points learned from these results]
|
|
167
|
-
|
|
168
|
-
## Observations
|
|
169
|
-
- [insight] [Important observation from search results]
|
|
170
|
-
- [connection] [How this connects to other topics]
|
|
171
|
-
|
|
172
|
-
## Relations
|
|
173
|
-
- relates_to [[{results.results[0].title if results.results else "Related Topic"}]]
|
|
174
|
-
- extends [[Another Relevant Topic]]
|
|
175
|
-
'''
|
|
176
|
-
)
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Remember that capturing synthesized knowledge is one of the most valuable features of Basic Memory.
|
|
180
|
-
""")
|
|
49
|
+
# Call the prompt API endpoint
|
|
50
|
+
response = await call_post(
|
|
51
|
+
client, f"{project_url}/prompt/search", json=request.model_dump(exclude_none=True)
|
|
52
|
+
)
|
|
181
53
|
|
|
182
|
-
|
|
54
|
+
# Extract the rendered prompt from the response
|
|
55
|
+
result = response.json()
|
|
56
|
+
return result["prompt"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Sync status prompt for Basic Memory MCP server."""
|
|
2
|
+
|
|
3
|
+
from basic_memory.mcp.server import mcp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@mcp.prompt(
|
|
7
|
+
description="""Get sync status with recommendations for AI assistants.
|
|
8
|
+
|
|
9
|
+
This prompt provides both current sync status and guidance on how
|
|
10
|
+
AI assistants should respond when sync operations are in progress or completed.
|
|
11
|
+
""",
|
|
12
|
+
)
|
|
13
|
+
async def sync_status_prompt() -> str:
|
|
14
|
+
"""Get sync status with AI assistant guidance.
|
|
15
|
+
|
|
16
|
+
This prompt provides detailed sync status information along with
|
|
17
|
+
recommendations for how AI assistants should handle different sync states.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Formatted sync status with AI assistant guidance
|
|
21
|
+
"""
|
|
22
|
+
try: # pragma: no cover
|
|
23
|
+
from basic_memory.services.migration_service import migration_manager
|
|
24
|
+
|
|
25
|
+
state = migration_manager.state
|
|
26
|
+
|
|
27
|
+
# Build status report
|
|
28
|
+
lines = [
|
|
29
|
+
"# Basic Memory Sync Status",
|
|
30
|
+
"",
|
|
31
|
+
f"**Current Status**: {state.status.value.replace('_', ' ').title()}",
|
|
32
|
+
f"**System Ready**: {'Yes' if migration_manager.is_ready else 'No'}",
|
|
33
|
+
"",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
if migration_manager.is_ready:
|
|
37
|
+
lines.extend(
|
|
38
|
+
[
|
|
39
|
+
"✅ **All sync operations completed** - System is fully operational",
|
|
40
|
+
"",
|
|
41
|
+
"All Basic Memory tools are available and functioning normally.",
|
|
42
|
+
"File indexing is complete and knowledge graphs are up to date.",
|
|
43
|
+
"You can proceed with any knowledge management tasks.",
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
lines.append(f"**Status Message**: {state.message}")
|
|
48
|
+
|
|
49
|
+
if state.status.value == "in_progress":
|
|
50
|
+
if state.projects_total > 0:
|
|
51
|
+
progress = f" ({state.projects_migrated}/{state.projects_total})"
|
|
52
|
+
lines.append(f"**Progress**: {progress}")
|
|
53
|
+
|
|
54
|
+
lines.extend(
|
|
55
|
+
[
|
|
56
|
+
"",
|
|
57
|
+
"🔄 **File synchronization in progress** - Processing files and building knowledge graphs",
|
|
58
|
+
"",
|
|
59
|
+
"**Impact**: Some tools may show status messages instead of normal responses",
|
|
60
|
+
"until sync completes (usually 1-3 minutes).",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
elif state.status.value == "failed":
|
|
65
|
+
lines.extend(
|
|
66
|
+
[
|
|
67
|
+
"",
|
|
68
|
+
f"❌ **Sync failed**: {state.error or 'Unknown error'}",
|
|
69
|
+
"",
|
|
70
|
+
"**Impact**: System may have limited functionality until issue is resolved.",
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Add AI assistant recommendations
|
|
75
|
+
if not migration_manager.is_ready:
|
|
76
|
+
lines.extend(
|
|
77
|
+
[
|
|
78
|
+
"",
|
|
79
|
+
"---",
|
|
80
|
+
"",
|
|
81
|
+
"## AI Assistant Recommendations",
|
|
82
|
+
"",
|
|
83
|
+
"**When sync is in progress:**",
|
|
84
|
+
"- Inform the user about the background file processing",
|
|
85
|
+
"- Suggest using `sync_status()` tool to check progress",
|
|
86
|
+
"- Explain that tools will work normally once sync completes",
|
|
87
|
+
"- Avoid creating complex workflows until sync is done",
|
|
88
|
+
"",
|
|
89
|
+
"**What to tell users:**",
|
|
90
|
+
"- 'Basic Memory is processing your files and building knowledge graphs'",
|
|
91
|
+
"- 'This usually takes 1-3 minutes depending on your content size'",
|
|
92
|
+
"- 'You can check progress anytime with the sync_status tool'",
|
|
93
|
+
"- 'Full functionality will be available once processing completes'",
|
|
94
|
+
"",
|
|
95
|
+
"**User-friendly language:**",
|
|
96
|
+
"- Say 'processing files' instead of 'migration' or 'sync'",
|
|
97
|
+
"- Say 'building knowledge graphs' instead of 'indexing'",
|
|
98
|
+
"- Say 'setting up your knowledge base' instead of 'running migrations'",
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
except Exception as e: # pragma: no cover
|
|
105
|
+
return f"""# Sync Status - Error
|
|
106
|
+
|
|
107
|
+
❌ **Unable to check sync status**: {str(e)}
|
|
108
|
+
|
|
109
|
+
## AI Assistant Recommendations
|
|
110
|
+
|
|
111
|
+
**When status is unavailable:**
|
|
112
|
+
- Assume the system is likely working normally
|
|
113
|
+
- Try proceeding with normal operations
|
|
114
|
+
- If users report issues, suggest checking logs or restarting
|
|
115
|
+
- Use user-friendly language about 'setting up the knowledge base'
|
|
116
|
+
"""
|
|
@@ -35,7 +35,7 @@ def format_prompt_context(context: PromptContext) -> str:
|
|
|
35
35
|
Returns:
|
|
36
36
|
Formatted continuation summary
|
|
37
37
|
"""
|
|
38
|
-
if not context.results:
|
|
38
|
+
if not context.results: # pragma: no cover
|
|
39
39
|
return dedent(f"""
|
|
40
40
|
# Continuing conversation on: {context.topic}
|
|
41
41
|
|
|
@@ -138,11 +138,11 @@ def format_prompt_context(context: PromptContext) -> str:
|
|
|
138
138
|
- type: **{related.type}**
|
|
139
139
|
- title: {related.title}
|
|
140
140
|
""")
|
|
141
|
-
if related.permalink:
|
|
141
|
+
if related.permalink: # pragma: no cover
|
|
142
142
|
section_content += (
|
|
143
143
|
f'You can view this document with: `read_note("{related.permalink}")`'
|
|
144
144
|
)
|
|
145
|
-
else:
|
|
145
|
+
else: # pragma: no cover
|
|
146
146
|
section_content += (
|
|
147
147
|
f'You can view this file with: `read_file("{related.file_path}")`'
|
|
148
148
|
)
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
4
|
|
|
5
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
5
6
|
from basic_memory.mcp.async_client import client
|
|
6
7
|
from basic_memory.mcp.server import mcp
|
|
7
8
|
from basic_memory.mcp.tools.utils import call_get
|
|
8
9
|
from basic_memory.schemas import ProjectInfoResponse
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
@mcp.
|
|
12
|
+
@mcp.resource(
|
|
13
|
+
uri="memory://project_info",
|
|
12
14
|
description="Get information and statistics about the current Basic Memory project.",
|
|
13
15
|
)
|
|
14
16
|
async def project_info() -> ProjectInfoResponse:
|
|
@@ -43,9 +45,11 @@ async def project_info() -> ProjectInfoResponse:
|
|
|
43
45
|
print(f"Basic Memory version: {info.system.version}")
|
|
44
46
|
"""
|
|
45
47
|
logger.info("Getting project info")
|
|
48
|
+
project_config = get_active_project()
|
|
49
|
+
project_url = project_config.project_url
|
|
46
50
|
|
|
47
51
|
# Call the API endpoint
|
|
48
|
-
response = await call_get(client, "/
|
|
52
|
+
response = await call_get(client, f"{project_url}/project/info")
|
|
49
53
|
|
|
50
54
|
# Convert response to ProjectInfoResponse
|
|
51
55
|
return ProjectInfoResponse.model_validate(response.json())
|
basic_memory/mcp/server.py
CHANGED
|
@@ -1,37 +1,110 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Basic Memory FastMCP server.
|
|
3
|
+
"""
|
|
2
4
|
|
|
3
5
|
import asyncio
|
|
4
6
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import AsyncIterator, Optional
|
|
6
|
-
|
|
7
|
-
from mcp.server.fastmcp import FastMCP
|
|
8
|
-
from mcp.server.fastmcp.utilities.logging import configure_logging as mcp_configure_logging
|
|
9
7
|
from dataclasses import dataclass
|
|
8
|
+
from typing import AsyncIterator, Optional, Any
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
from fastmcp.utilities.logging import configure_logging as mcp_configure_logging
|
|
13
|
+
from mcp.server.auth.settings import AuthSettings
|
|
10
14
|
|
|
11
|
-
from basic_memory.config import
|
|
15
|
+
from basic_memory.config import app_config
|
|
12
16
|
from basic_memory.services.initialization import initialize_app
|
|
17
|
+
from basic_memory.mcp.auth_provider import BasicMemoryOAuthProvider
|
|
18
|
+
from basic_memory.mcp.project_session import session
|
|
19
|
+
from basic_memory.mcp.external_auth_provider import (
|
|
20
|
+
create_github_provider,
|
|
21
|
+
create_google_provider,
|
|
22
|
+
)
|
|
23
|
+
from basic_memory.mcp.supabase_auth_provider import SupabaseOAuthProvider
|
|
13
24
|
|
|
14
25
|
# mcp console logging
|
|
15
26
|
mcp_configure_logging(level="ERROR")
|
|
16
27
|
|
|
28
|
+
load_dotenv()
|
|
29
|
+
|
|
17
30
|
|
|
18
31
|
@dataclass
|
|
19
32
|
class AppContext:
|
|
20
33
|
watch_task: Optional[asyncio.Task]
|
|
34
|
+
migration_manager: Optional[Any] = None
|
|
21
35
|
|
|
22
36
|
|
|
23
37
|
@asynccontextmanager
|
|
24
38
|
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
|
|
25
39
|
"""Manage application lifecycle with type-safe context"""
|
|
26
|
-
# Initialize on startup
|
|
27
|
-
|
|
40
|
+
# Initialize on startup (now returns migration_manager)
|
|
41
|
+
migration_manager = await initialize_app(app_config)
|
|
42
|
+
|
|
43
|
+
# Initialize project session with default project
|
|
44
|
+
session.initialize(app_config.default_project)
|
|
45
|
+
|
|
28
46
|
try:
|
|
29
|
-
yield AppContext(watch_task=
|
|
47
|
+
yield AppContext(watch_task=None, migration_manager=migration_manager)
|
|
30
48
|
finally:
|
|
31
|
-
# Cleanup on shutdown
|
|
32
|
-
|
|
33
|
-
|
|
49
|
+
# Cleanup on shutdown - migration tasks will be cancelled automatically
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# OAuth configuration function
|
|
54
|
+
def create_auth_config() -> tuple[AuthSettings | None, Any | None]:
|
|
55
|
+
"""Create OAuth configuration if enabled."""
|
|
56
|
+
# Check if OAuth is enabled via environment variable
|
|
57
|
+
import os
|
|
58
|
+
|
|
59
|
+
if os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true":
|
|
60
|
+
from pydantic import AnyHttpUrl
|
|
61
|
+
|
|
62
|
+
# Configure OAuth settings
|
|
63
|
+
issuer_url = os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000")
|
|
64
|
+
required_scopes = os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES", "read,write")
|
|
65
|
+
docs_url = os.getenv("FASTMCP_AUTH_DOCS_URL") or "http://localhost:8000/docs/oauth"
|
|
66
|
+
|
|
67
|
+
auth_settings = AuthSettings(
|
|
68
|
+
issuer_url=AnyHttpUrl(issuer_url),
|
|
69
|
+
service_documentation_url=AnyHttpUrl(docs_url),
|
|
70
|
+
required_scopes=required_scopes.split(",") if required_scopes else ["read", "write"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Create OAuth provider based on type
|
|
74
|
+
provider_type = os.getenv("FASTMCP_AUTH_PROVIDER", "basic").lower()
|
|
75
|
+
|
|
76
|
+
if provider_type == "github":
|
|
77
|
+
auth_provider = create_github_provider()
|
|
78
|
+
elif provider_type == "google":
|
|
79
|
+
auth_provider = create_google_provider()
|
|
80
|
+
elif provider_type == "supabase":
|
|
81
|
+
supabase_url = os.getenv("SUPABASE_URL")
|
|
82
|
+
supabase_anon_key = os.getenv("SUPABASE_ANON_KEY")
|
|
83
|
+
supabase_service_key = os.getenv("SUPABASE_SERVICE_KEY")
|
|
84
|
+
|
|
85
|
+
if not supabase_url or not supabase_anon_key:
|
|
86
|
+
raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY must be set for Supabase auth")
|
|
87
|
+
|
|
88
|
+
auth_provider = SupabaseOAuthProvider(
|
|
89
|
+
supabase_url=supabase_url,
|
|
90
|
+
supabase_anon_key=supabase_anon_key,
|
|
91
|
+
supabase_service_key=supabase_service_key,
|
|
92
|
+
issuer_url=issuer_url,
|
|
93
|
+
)
|
|
94
|
+
else: # default to "basic"
|
|
95
|
+
auth_provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
|
|
96
|
+
|
|
97
|
+
return auth_settings, auth_provider
|
|
98
|
+
|
|
99
|
+
return None, None
|
|
100
|
+
|
|
34
101
|
|
|
102
|
+
# Create auth configuration
|
|
103
|
+
auth_settings, auth_provider = create_auth_config()
|
|
35
104
|
|
|
36
105
|
# Create the shared server instance
|
|
37
|
-
mcp = FastMCP(
|
|
106
|
+
mcp = FastMCP(
|
|
107
|
+
name="Basic Memory",
|
|
108
|
+
log_level="DEBUG",
|
|
109
|
+
auth=auth_provider,
|
|
110
|
+
)
|