basic-memory 0.8.0__py3-none-any.whl → 0.10.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 +1 -1
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
- basic_memory/api/app.py +9 -6
- basic_memory/api/routers/__init__.py +2 -1
- basic_memory/api/routers/knowledge_router.py +30 -4
- basic_memory/api/routers/memory_router.py +3 -2
- basic_memory/api/routers/project_info_router.py +274 -0
- basic_memory/api/routers/search_router.py +22 -4
- basic_memory/cli/app.py +54 -3
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/db.py +9 -13
- basic_memory/cli/commands/import_chatgpt.py +31 -36
- basic_memory/cli/commands/import_claude_conversations.py +32 -35
- basic_memory/cli/commands/import_claude_projects.py +34 -37
- basic_memory/cli/commands/import_memory_json.py +26 -28
- basic_memory/cli/commands/mcp.py +7 -1
- basic_memory/cli/commands/project.py +119 -0
- basic_memory/cli/commands/project_info.py +167 -0
- basic_memory/cli/commands/status.py +7 -9
- basic_memory/cli/commands/sync.py +54 -9
- basic_memory/cli/commands/{tools.py → tool.py} +92 -19
- basic_memory/cli/main.py +40 -1
- basic_memory/config.py +157 -10
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +34 -18
- basic_memory/markdown/markdown_processor.py +1 -1
- basic_memory/markdown/utils.py +5 -0
- basic_memory/mcp/main.py +1 -2
- basic_memory/mcp/prompts/__init__.py +6 -2
- basic_memory/mcp/prompts/ai_assistant_guide.py +9 -10
- basic_memory/mcp/prompts/continue_conversation.py +65 -126
- basic_memory/mcp/prompts/recent_activity.py +55 -13
- basic_memory/mcp/prompts/search.py +72 -17
- basic_memory/mcp/prompts/utils.py +139 -82
- basic_memory/mcp/server.py +1 -1
- basic_memory/mcp/tools/__init__.py +11 -22
- basic_memory/mcp/tools/build_context.py +85 -0
- basic_memory/mcp/tools/canvas.py +17 -19
- basic_memory/mcp/tools/delete_note.py +28 -0
- basic_memory/mcp/tools/project_info.py +51 -0
- basic_memory/mcp/tools/{resource.py → read_content.py} +42 -5
- basic_memory/mcp/tools/read_note.py +190 -0
- basic_memory/mcp/tools/recent_activity.py +100 -0
- basic_memory/mcp/tools/search.py +56 -17
- basic_memory/mcp/tools/utils.py +245 -17
- basic_memory/mcp/tools/write_note.py +124 -0
- basic_memory/models/search.py +2 -1
- basic_memory/repository/entity_repository.py +3 -2
- basic_memory/repository/project_info_repository.py +9 -0
- basic_memory/repository/repository.py +23 -6
- basic_memory/repository/search_repository.py +33 -10
- basic_memory/schemas/__init__.py +12 -0
- basic_memory/schemas/memory.py +3 -2
- basic_memory/schemas/project_info.py +96 -0
- basic_memory/schemas/search.py +27 -32
- basic_memory/services/context_service.py +3 -3
- basic_memory/services/entity_service.py +8 -2
- basic_memory/services/file_service.py +107 -57
- basic_memory/services/link_resolver.py +5 -45
- basic_memory/services/search_service.py +45 -16
- basic_memory/sync/sync_service.py +274 -39
- basic_memory/sync/watch_service.py +174 -34
- basic_memory/utils.py +40 -40
- basic_memory-0.10.0.dist-info/METADATA +386 -0
- basic_memory-0.10.0.dist-info/RECORD +99 -0
- basic_memory/mcp/prompts/json_canvas_spec.py +0 -25
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -177
- basic_memory/mcp/tools/notes.py +0 -201
- basic_memory-0.8.0.dist-info/METADATA +0 -379
- basic_memory-0.8.0.dist-info/RECORD +0 -91
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.8.0.dist-info → basic_memory-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Read note tool for Basic Memory MCP server."""
|
|
2
|
+
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from basic_memory.mcp.async_client import client
|
|
8
|
+
from basic_memory.mcp.server import mcp
|
|
9
|
+
from basic_memory.mcp.tools.search import search
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
11
|
+
from basic_memory.schemas.memory import memory_url_path
|
|
12
|
+
from basic_memory.schemas.search import SearchQuery
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@mcp.tool(
|
|
16
|
+
description="Read a markdown note by title or permalink.",
|
|
17
|
+
)
|
|
18
|
+
async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
19
|
+
"""Read a markdown note from the knowledge base.
|
|
20
|
+
|
|
21
|
+
This tool finds and retrieves a note by its title, permalink, or content search,
|
|
22
|
+
returning the raw markdown content including observations, relations, and metadata.
|
|
23
|
+
It will try multiple lookup strategies to find the most relevant note.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
identifier: The title or permalink of the note to read
|
|
27
|
+
Can be a full memory:// URL, a permalink, a title, or search text
|
|
28
|
+
page: Page number for paginated results (default: 1)
|
|
29
|
+
page_size: Number of items per page (default: 10)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The full markdown content of the note if found, or helpful guidance if not found.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
# Read by permalink
|
|
36
|
+
read_note("specs/search-spec")
|
|
37
|
+
|
|
38
|
+
# Read by title
|
|
39
|
+
read_note("Search Specification")
|
|
40
|
+
|
|
41
|
+
# Read with memory URL
|
|
42
|
+
read_note("memory://specs/search-spec")
|
|
43
|
+
|
|
44
|
+
# Read with pagination
|
|
45
|
+
read_note("Project Updates", page=2, page_size=5)
|
|
46
|
+
"""
|
|
47
|
+
# Get the file via REST API - first try direct permalink lookup
|
|
48
|
+
entity_path = memory_url_path(identifier)
|
|
49
|
+
path = f"/resource/{entity_path}"
|
|
50
|
+
logger.info(f"Attempting to read note from URL: {path}")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Try direct lookup first
|
|
54
|
+
response = await call_get(client, path, params={"page": page, "page_size": page_size})
|
|
55
|
+
|
|
56
|
+
# If successful, return the content
|
|
57
|
+
if response.status_code == 200:
|
|
58
|
+
logger.info("Returning read_note result from resource: {path}", path=entity_path)
|
|
59
|
+
return response.text
|
|
60
|
+
except Exception as e: # pragma: no cover
|
|
61
|
+
logger.info(f"Direct lookup failed for '{path}': {e}")
|
|
62
|
+
# Continue to fallback methods
|
|
63
|
+
|
|
64
|
+
# Fallback 1: Try title search via API
|
|
65
|
+
logger.info(f"Search title for: {identifier}")
|
|
66
|
+
title_results = await search(SearchQuery(title=identifier))
|
|
67
|
+
|
|
68
|
+
if title_results and title_results.results:
|
|
69
|
+
result = title_results.results[0] # Get the first/best match
|
|
70
|
+
if result.permalink:
|
|
71
|
+
try:
|
|
72
|
+
# Try to fetch the content using the found permalink
|
|
73
|
+
path = f"/resource/{result.permalink}"
|
|
74
|
+
response = await call_get(
|
|
75
|
+
client, path, params={"page": page, "page_size": page_size}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if response.status_code == 200:
|
|
79
|
+
logger.info(f"Found note by title search: {result.permalink}")
|
|
80
|
+
return response.text
|
|
81
|
+
except Exception as e: # pragma: no cover
|
|
82
|
+
logger.info(
|
|
83
|
+
f"Failed to fetch content for found title match {result.permalink}: {e}"
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
logger.info(f"No results in title search for: {identifier}")
|
|
87
|
+
|
|
88
|
+
# Fallback 2: Text search as a last resort
|
|
89
|
+
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
90
|
+
text_results = await search(SearchQuery(text=identifier))
|
|
91
|
+
|
|
92
|
+
# We didn't find a direct match, construct a helpful error message
|
|
93
|
+
if not text_results or not text_results.results:
|
|
94
|
+
# No results at all
|
|
95
|
+
return format_not_found_message(identifier)
|
|
96
|
+
else:
|
|
97
|
+
# We found some related results
|
|
98
|
+
return format_related_results(identifier, text_results.results[:5])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def format_not_found_message(identifier: str) -> str:
|
|
102
|
+
"""Format a helpful message when no note was found."""
|
|
103
|
+
return dedent(f"""
|
|
104
|
+
# Note Not Found: "{identifier}"
|
|
105
|
+
|
|
106
|
+
I couldn't find any notes matching "{identifier}". Here are some suggestions:
|
|
107
|
+
|
|
108
|
+
## Check Identifier Type
|
|
109
|
+
- If you provided a title, try using the exact permalink instead
|
|
110
|
+
- If you provided a permalink, check for typos or try a broader search
|
|
111
|
+
|
|
112
|
+
## Search Instead
|
|
113
|
+
Try searching for related content:
|
|
114
|
+
```
|
|
115
|
+
search(query="{identifier}")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Recent Activity
|
|
119
|
+
Check recently modified notes:
|
|
120
|
+
```
|
|
121
|
+
recent_activity(timeframe="7d")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Create New Note
|
|
125
|
+
This might be a good opportunity to create a new note on this topic:
|
|
126
|
+
```
|
|
127
|
+
write_note(
|
|
128
|
+
title="{identifier.capitalize()}",
|
|
129
|
+
content='''
|
|
130
|
+
# {identifier.capitalize()}
|
|
131
|
+
|
|
132
|
+
## Overview
|
|
133
|
+
[Your content here]
|
|
134
|
+
|
|
135
|
+
## Observations
|
|
136
|
+
- [category] [Observation about {identifier}]
|
|
137
|
+
|
|
138
|
+
## Relations
|
|
139
|
+
- relates_to [[Related Topic]]
|
|
140
|
+
''',
|
|
141
|
+
folder="notes"
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
""")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_related_results(identifier: str, results) -> str:
|
|
148
|
+
"""Format a helpful message with related results when an exact match wasn't found."""
|
|
149
|
+
message = dedent(f"""
|
|
150
|
+
# Note Not Found: "{identifier}"
|
|
151
|
+
|
|
152
|
+
I couldn't find an exact match for "{identifier}", but I found some related notes:
|
|
153
|
+
|
|
154
|
+
""")
|
|
155
|
+
|
|
156
|
+
for i, result in enumerate(results):
|
|
157
|
+
message += dedent(f"""
|
|
158
|
+
## {i + 1}. {result.title}
|
|
159
|
+
- **Type**: {result.type.value}
|
|
160
|
+
- **Permalink**: {result.permalink}
|
|
161
|
+
|
|
162
|
+
You can read this note with:
|
|
163
|
+
```
|
|
164
|
+
read_note("{result.permalink}")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
""")
|
|
168
|
+
|
|
169
|
+
message += dedent("""
|
|
170
|
+
## Try More Specific Lookup
|
|
171
|
+
For exact matches, try using the full permalink from one of the results above.
|
|
172
|
+
|
|
173
|
+
## Search For More Results
|
|
174
|
+
To see more related content:
|
|
175
|
+
```
|
|
176
|
+
search(query="{identifier}")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Create New Note
|
|
180
|
+
If none of these match what you're looking for, consider creating a new note:
|
|
181
|
+
```
|
|
182
|
+
write_note(
|
|
183
|
+
title="[Your title]",
|
|
184
|
+
content="[Your content]",
|
|
185
|
+
folder="notes"
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
""")
|
|
189
|
+
|
|
190
|
+
return message
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Recent activity tool for Basic Memory MCP server."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from basic_memory.mcp.async_client import client
|
|
8
|
+
from basic_memory.mcp.server import mcp
|
|
9
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
10
|
+
from basic_memory.schemas.base import TimeFrame
|
|
11
|
+
from basic_memory.schemas.memory import GraphContext
|
|
12
|
+
from basic_memory.schemas.search import SearchItemType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@mcp.tool(
|
|
16
|
+
description="""Get recent activity from across the knowledge base.
|
|
17
|
+
|
|
18
|
+
Timeframe supports natural language formats like:
|
|
19
|
+
- "2 days ago"
|
|
20
|
+
- "last week"
|
|
21
|
+
- "yesterday"
|
|
22
|
+
- "today"
|
|
23
|
+
- "3 weeks ago"
|
|
24
|
+
Or standard formats like "7d"
|
|
25
|
+
""",
|
|
26
|
+
)
|
|
27
|
+
async def recent_activity(
|
|
28
|
+
type: Optional[List[SearchItemType]] = None,
|
|
29
|
+
depth: Optional[int] = 1,
|
|
30
|
+
timeframe: Optional[TimeFrame] = "7d",
|
|
31
|
+
page: int = 1,
|
|
32
|
+
page_size: int = 10,
|
|
33
|
+
max_related: int = 10,
|
|
34
|
+
) -> GraphContext:
|
|
35
|
+
"""Get recent activity across the knowledge base.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
type: Filter by content type(s). Valid options:
|
|
39
|
+
- ["entity"] for knowledge entities
|
|
40
|
+
- ["relation"] for connections between entities
|
|
41
|
+
- ["observation"] for notes and observations
|
|
42
|
+
Multiple types can be combined: ["entity", "relation"]
|
|
43
|
+
depth: How many relation hops to traverse (1-3 recommended)
|
|
44
|
+
timeframe: Time window to search. Supports natural language:
|
|
45
|
+
- Relative: "2 days ago", "last week", "yesterday"
|
|
46
|
+
- Points in time: "2024-01-01", "January 1st"
|
|
47
|
+
- Standard format: "7d", "24h"
|
|
48
|
+
page: Page number of results to return (default: 1)
|
|
49
|
+
page_size: Number of results to return per page (default: 10)
|
|
50
|
+
max_related: Maximum number of related results to return (default: 10)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
GraphContext containing:
|
|
54
|
+
- primary_results: Latest activities matching the filters
|
|
55
|
+
- related_results: Connected content via relations
|
|
56
|
+
- metadata: Query details and statistics
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
# Get all entities for the last 10 days (default)
|
|
60
|
+
recent_activity()
|
|
61
|
+
|
|
62
|
+
# Get all entities from yesterday
|
|
63
|
+
recent_activity(type=["entity"], timeframe="yesterday")
|
|
64
|
+
|
|
65
|
+
# Get recent relations and observations
|
|
66
|
+
recent_activity(type=["relation", "observation"], timeframe="today")
|
|
67
|
+
|
|
68
|
+
# Look back further with more context
|
|
69
|
+
recent_activity(type=["entity"], depth=2, timeframe="2 weeks ago")
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
- Higher depth values (>3) may impact performance with large result sets
|
|
73
|
+
- For focused queries, consider using build_context with a specific URI
|
|
74
|
+
- Max timeframe is 1 year in the past
|
|
75
|
+
"""
|
|
76
|
+
logger.info(
|
|
77
|
+
f"Getting recent activity from type={type}, depth={depth}, timeframe={timeframe}, page={page}, page_size={page_size}, max_related={max_related}"
|
|
78
|
+
)
|
|
79
|
+
params = {
|
|
80
|
+
"page": page,
|
|
81
|
+
"page_size": page_size,
|
|
82
|
+
"max_related": max_related,
|
|
83
|
+
}
|
|
84
|
+
if depth:
|
|
85
|
+
params["depth"] = depth
|
|
86
|
+
if timeframe:
|
|
87
|
+
params["timeframe"] = timeframe # pyright: ignore
|
|
88
|
+
|
|
89
|
+
# send enum values if we have an enum, else send string value
|
|
90
|
+
if type:
|
|
91
|
+
params["type"] = [ # pyright: ignore
|
|
92
|
+
type.value if isinstance(type, SearchItemType) else type for type in type
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
response = await call_get(
|
|
96
|
+
client,
|
|
97
|
+
"/memory/recent",
|
|
98
|
+
params=params,
|
|
99
|
+
)
|
|
100
|
+
return GraphContext.model_validate(response.json())
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Search tools for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
-
import logfire
|
|
4
3
|
from loguru import logger
|
|
5
4
|
|
|
6
5
|
from basic_memory.mcp.server import mcp
|
|
@@ -15,24 +14,64 @@ from basic_memory.mcp.async_client import client
|
|
|
15
14
|
async def search(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
|
|
16
15
|
"""Search across all content in basic-memory.
|
|
17
16
|
|
|
17
|
+
This tool searches the knowledge base using full-text search, pattern matching,
|
|
18
|
+
or exact permalink lookup. It supports filtering by content type, entity type,
|
|
19
|
+
and date.
|
|
20
|
+
|
|
18
21
|
Args:
|
|
19
22
|
query: SearchQuery object with search parameters including:
|
|
20
|
-
- text:
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
- text: Full-text search (e.g., "project planning")
|
|
24
|
+
Supports boolean operators: AND, OR, NOT and parentheses for grouping
|
|
25
|
+
- title: Search only in titles (e.g., "Meeting notes")
|
|
26
|
+
- permalink: Exact permalink match (e.g., "docs/meeting-notes")
|
|
27
|
+
- permalink_match: Pattern matching for permalinks (e.g., "docs/*-notes")
|
|
28
|
+
- types: Optional list of content types to search (e.g., ["entity", "observation"])
|
|
29
|
+
- entity_types: Optional list of entity types to filter by (e.g., ["note", "person"])
|
|
30
|
+
- after_date: Optional date filter for recent content (e.g., "1 week", "2d")
|
|
31
|
+
page: The page number of results to return (default 1)
|
|
32
|
+
page_size: The number of results to return per page (default 10)
|
|
26
33
|
|
|
27
34
|
Returns:
|
|
28
|
-
SearchResponse with
|
|
35
|
+
SearchResponse with results and pagination info
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
# Basic text search
|
|
39
|
+
results = await search(SearchQuery(text="project planning"))
|
|
40
|
+
|
|
41
|
+
# Boolean AND search (both terms must be present)
|
|
42
|
+
results = await search(SearchQuery(text="project AND planning"))
|
|
43
|
+
|
|
44
|
+
# Boolean OR search (either term can be present)
|
|
45
|
+
results = await search(SearchQuery(text="project OR meeting"))
|
|
46
|
+
|
|
47
|
+
# Boolean NOT search (exclude terms)
|
|
48
|
+
results = await search(SearchQuery(text="project NOT meeting"))
|
|
49
|
+
|
|
50
|
+
# Boolean search with grouping
|
|
51
|
+
results = await search(SearchQuery(text="(project OR planning) AND notes"))
|
|
52
|
+
|
|
53
|
+
# Search with type filter
|
|
54
|
+
results = await search(SearchQuery(
|
|
55
|
+
text="meeting notes",
|
|
56
|
+
types=["entity"],
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
# Search for recent content
|
|
60
|
+
results = await search(SearchQuery(
|
|
61
|
+
text="bug report",
|
|
62
|
+
after_date="1 week"
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
# Pattern matching on permalinks
|
|
66
|
+
results = await search(SearchQuery(
|
|
67
|
+
permalink_match="docs/meeting-*"
|
|
68
|
+
))
|
|
29
69
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return SearchResponse.model_validate(response.json())
|
|
70
|
+
logger.info(f"Searching for {query}")
|
|
71
|
+
response = await call_post(
|
|
72
|
+
client,
|
|
73
|
+
"/search/",
|
|
74
|
+
json=query.model_dump(),
|
|
75
|
+
params={"page": page, "page_size": page_size},
|
|
76
|
+
)
|
|
77
|
+
return SearchResponse.model_validate(response.json())
|