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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Read note tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
3
|
from textwrap import dedent
|
|
4
|
+
from typing import Optional
|
|
4
5
|
|
|
5
6
|
from loguru import logger
|
|
6
7
|
|
|
@@ -8,13 +9,16 @@ from basic_memory.mcp.async_client import client
|
|
|
8
9
|
from basic_memory.mcp.server import mcp
|
|
9
10
|
from basic_memory.mcp.tools.search import search_notes
|
|
10
11
|
from basic_memory.mcp.tools.utils import call_get
|
|
12
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
11
13
|
from basic_memory.schemas.memory import memory_url_path
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@mcp.tool(
|
|
15
17
|
description="Read a markdown note by title or permalink.",
|
|
16
18
|
)
|
|
17
|
-
async def read_note(
|
|
19
|
+
async def read_note(
|
|
20
|
+
identifier: str, page: int = 1, page_size: int = 10, project: Optional[str] = None
|
|
21
|
+
) -> str:
|
|
18
22
|
"""Read a markdown note from the knowledge base.
|
|
19
23
|
|
|
20
24
|
This tool finds and retrieves a note by its title, permalink, or content search,
|
|
@@ -26,6 +30,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
26
30
|
Can be a full memory:// URL, a permalink, a title, or search text
|
|
27
31
|
page: Page number for paginated results (default: 1)
|
|
28
32
|
page_size: Number of items per page (default: 10)
|
|
33
|
+
project: Optional project name to read from. If not provided, uses current active project.
|
|
29
34
|
|
|
30
35
|
Returns:
|
|
31
36
|
The full markdown content of the note if found, or helpful guidance if not found.
|
|
@@ -42,10 +47,24 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
42
47
|
|
|
43
48
|
# Read with pagination
|
|
44
49
|
read_note("Project Updates", page=2, page_size=5)
|
|
50
|
+
|
|
51
|
+
# Read from specific project
|
|
52
|
+
read_note("Meeting Notes", project="work-project")
|
|
45
53
|
"""
|
|
54
|
+
|
|
55
|
+
# Check migration status and wait briefly if needed
|
|
56
|
+
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
57
|
+
|
|
58
|
+
migration_status = await wait_for_migration_or_return_status(timeout=5.0)
|
|
59
|
+
if migration_status: # pragma: no cover
|
|
60
|
+
return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before reading notes."
|
|
61
|
+
|
|
62
|
+
active_project = get_active_project(project)
|
|
63
|
+
project_url = active_project.project_url
|
|
64
|
+
|
|
46
65
|
# Get the file via REST API - first try direct permalink lookup
|
|
47
66
|
entity_path = memory_url_path(identifier)
|
|
48
|
-
path = f"/resource/{entity_path}"
|
|
67
|
+
path = f"{project_url}/resource/{entity_path}"
|
|
49
68
|
logger.info(f"Attempting to read note from URL: {path}")
|
|
50
69
|
|
|
51
70
|
try:
|
|
@@ -62,14 +81,14 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
62
81
|
|
|
63
82
|
# Fallback 1: Try title search via API
|
|
64
83
|
logger.info(f"Search title for: {identifier}")
|
|
65
|
-
title_results = await search_notes(query=identifier, search_type="title")
|
|
84
|
+
title_results = await search_notes.fn(query=identifier, search_type="title", project=project)
|
|
66
85
|
|
|
67
86
|
if title_results and title_results.results:
|
|
68
87
|
result = title_results.results[0] # Get the first/best match
|
|
69
88
|
if result.permalink:
|
|
70
89
|
try:
|
|
71
90
|
# Try to fetch the content using the found permalink
|
|
72
|
-
path = f"/resource/{result.permalink}"
|
|
91
|
+
path = f"{project_url}/resource/{result.permalink}"
|
|
73
92
|
response = await call_get(
|
|
74
93
|
client, path, params={"page": page, "page_size": page_size}
|
|
75
94
|
)
|
|
@@ -86,7 +105,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
86
105
|
|
|
87
106
|
# Fallback 2: Text search as a last resort
|
|
88
107
|
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
89
|
-
text_results = await search_notes(query=identifier, search_type="text")
|
|
108
|
+
text_results = await search_notes.fn(query=identifier, search_type="text", project=project)
|
|
90
109
|
|
|
91
110
|
# We didn't find a direct match, construct a helpful error message
|
|
92
111
|
if not text_results or not text_results.results:
|
|
@@ -102,7 +121,7 @@ def format_not_found_message(identifier: str) -> str:
|
|
|
102
121
|
return dedent(f"""
|
|
103
122
|
# Note Not Found: "{identifier}"
|
|
104
123
|
|
|
105
|
-
I couldn't find any
|
|
124
|
+
I searched for "{identifier}" using multiple methods (direct lookup, title search, and text search) but couldn't find any matching notes. Here are some suggestions:
|
|
106
125
|
|
|
107
126
|
## Check Identifier Type
|
|
108
127
|
- If you provided a title, try using the exact permalink instead
|
|
@@ -111,7 +130,7 @@ def format_not_found_message(identifier: str) -> str:
|
|
|
111
130
|
## Search Instead
|
|
112
131
|
Try searching for related content:
|
|
113
132
|
```
|
|
114
|
-
|
|
133
|
+
search_notes(query="{identifier}")
|
|
115
134
|
```
|
|
116
135
|
|
|
117
136
|
## Recent Activity
|
|
@@ -148,7 +167,7 @@ def format_related_results(identifier: str, results) -> str:
|
|
|
148
167
|
message = dedent(f"""
|
|
149
168
|
# Note Not Found: "{identifier}"
|
|
150
169
|
|
|
151
|
-
I couldn't find an exact match
|
|
170
|
+
I searched for "{identifier}" using direct lookup and title search but couldn't find an exact match. However, I found some related notes through text search:
|
|
152
171
|
|
|
153
172
|
""")
|
|
154
173
|
|
|
@@ -172,7 +191,7 @@ def format_related_results(identifier: str, results) -> str:
|
|
|
172
191
|
## Search For More Results
|
|
173
192
|
To see more related content:
|
|
174
193
|
```
|
|
175
|
-
|
|
194
|
+
search_notes(query="{identifier}")
|
|
176
195
|
```
|
|
177
196
|
|
|
178
197
|
## Create New Note
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Recent activity tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import List, Union, Optional
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
from basic_memory.mcp.async_client import client
|
|
8
8
|
from basic_memory.mcp.server import mcp
|
|
9
9
|
from basic_memory.mcp.tools.utils import call_get
|
|
10
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
10
11
|
from basic_memory.schemas.base import TimeFrame
|
|
11
12
|
from basic_memory.schemas.memory import GraphContext
|
|
12
13
|
from basic_memory.schemas.search import SearchItemType
|
|
@@ -14,7 +15,7 @@ from basic_memory.schemas.search import SearchItemType
|
|
|
14
15
|
|
|
15
16
|
@mcp.tool(
|
|
16
17
|
description="""Get recent activity from across the knowledge base.
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
Timeframe supports natural language formats like:
|
|
19
20
|
- "2 days ago"
|
|
20
21
|
- "last week"
|
|
@@ -25,21 +26,25 @@ from basic_memory.schemas.search import SearchItemType
|
|
|
25
26
|
""",
|
|
26
27
|
)
|
|
27
28
|
async def recent_activity(
|
|
28
|
-
type:
|
|
29
|
-
depth:
|
|
30
|
-
timeframe:
|
|
29
|
+
type: Union[str, List[str]] = "",
|
|
30
|
+
depth: int = 1,
|
|
31
|
+
timeframe: TimeFrame = "7d",
|
|
31
32
|
page: int = 1,
|
|
32
33
|
page_size: int = 10,
|
|
33
34
|
max_related: int = 10,
|
|
35
|
+
project: Optional[str] = None,
|
|
34
36
|
) -> GraphContext:
|
|
35
37
|
"""Get recent activity across the knowledge base.
|
|
36
38
|
|
|
37
39
|
Args:
|
|
38
|
-
type: Filter by content type(s).
|
|
39
|
-
|
|
40
|
-
- ["
|
|
41
|
-
- ["
|
|
40
|
+
type: Filter by content type(s). Can be a string or list of strings.
|
|
41
|
+
Valid options:
|
|
42
|
+
- "entity" or ["entity"] for knowledge entities
|
|
43
|
+
- "relation" or ["relation"] for connections between entities
|
|
44
|
+
- "observation" or ["observation"] for notes and observations
|
|
42
45
|
Multiple types can be combined: ["entity", "relation"]
|
|
46
|
+
Case-insensitive: "ENTITY" and "entity" are treated the same.
|
|
47
|
+
Default is an empty string, which returns all types.
|
|
43
48
|
depth: How many relation hops to traverse (1-3 recommended)
|
|
44
49
|
timeframe: Time window to search. Supports natural language:
|
|
45
50
|
- Relative: "2 days ago", "last week", "yesterday"
|
|
@@ -48,6 +53,7 @@ async def recent_activity(
|
|
|
48
53
|
page: Page number of results to return (default: 1)
|
|
49
54
|
page_size: Number of results to return per page (default: 10)
|
|
50
55
|
max_related: Maximum number of related results to return (default: 10)
|
|
56
|
+
project: Optional project name to get activity from. If not provided, uses current active project.
|
|
51
57
|
|
|
52
58
|
Returns:
|
|
53
59
|
GraphContext containing:
|
|
@@ -59,14 +65,20 @@ async def recent_activity(
|
|
|
59
65
|
# Get all entities for the last 10 days (default)
|
|
60
66
|
recent_activity()
|
|
61
67
|
|
|
62
|
-
# Get all entities from yesterday
|
|
68
|
+
# Get all entities from yesterday (string format)
|
|
69
|
+
recent_activity(type="entity", timeframe="yesterday")
|
|
70
|
+
|
|
71
|
+
# Get all entities from yesterday (list format)
|
|
63
72
|
recent_activity(type=["entity"], timeframe="yesterday")
|
|
64
73
|
|
|
65
74
|
# Get recent relations and observations
|
|
66
75
|
recent_activity(type=["relation", "observation"], timeframe="today")
|
|
67
76
|
|
|
68
77
|
# Look back further with more context
|
|
69
|
-
recent_activity(type=
|
|
78
|
+
recent_activity(type="entity", depth=2, timeframe="2 weeks ago")
|
|
79
|
+
|
|
80
|
+
# Get activity from specific project
|
|
81
|
+
recent_activity(type="entity", project="work-project")
|
|
70
82
|
|
|
71
83
|
Notes:
|
|
72
84
|
- Higher depth values (>3) may impact performance with large result sets
|
|
@@ -86,15 +98,34 @@ async def recent_activity(
|
|
|
86
98
|
if timeframe:
|
|
87
99
|
params["timeframe"] = timeframe # pyright: ignore
|
|
88
100
|
|
|
89
|
-
#
|
|
101
|
+
# Validate and convert type parameter
|
|
90
102
|
if type:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
# Convert single string to list
|
|
104
|
+
if isinstance(type, str):
|
|
105
|
+
type_list = [type]
|
|
106
|
+
else:
|
|
107
|
+
type_list = type
|
|
108
|
+
|
|
109
|
+
# Validate each type against SearchItemType enum
|
|
110
|
+
validated_types = []
|
|
111
|
+
for t in type_list:
|
|
112
|
+
try:
|
|
113
|
+
# Try to convert string to enum
|
|
114
|
+
if isinstance(t, str):
|
|
115
|
+
validated_types.append(SearchItemType(t.lower()))
|
|
116
|
+
except ValueError:
|
|
117
|
+
valid_types = [t.value for t in SearchItemType]
|
|
118
|
+
raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
|
|
119
|
+
|
|
120
|
+
# Add validated types to params
|
|
121
|
+
params["type"] = [t.value for t in validated_types] # pyright: ignore
|
|
122
|
+
|
|
123
|
+
active_project = get_active_project(project)
|
|
124
|
+
project_url = active_project.project_url
|
|
94
125
|
|
|
95
126
|
response = await call_get(
|
|
96
127
|
client,
|
|
97
|
-
"/memory/recent",
|
|
128
|
+
f"{project_url}/memory/recent",
|
|
98
129
|
params=params,
|
|
99
130
|
)
|
|
100
131
|
return GraphContext.model_validate(response.json())
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Search tools for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
+
from textwrap import dedent
|
|
3
4
|
from typing import List, Optional
|
|
4
5
|
|
|
5
6
|
from loguru import logger
|
|
@@ -7,9 +8,166 @@ from loguru import logger
|
|
|
7
8
|
from basic_memory.mcp.async_client import client
|
|
8
9
|
from basic_memory.mcp.server import mcp
|
|
9
10
|
from basic_memory.mcp.tools.utils import call_post
|
|
11
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
10
12
|
from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
def _format_search_error_response(error_message: str, query: str, search_type: str = "text") -> str:
|
|
16
|
+
"""Format helpful error responses for search failures that guide users to successful searches."""
|
|
17
|
+
|
|
18
|
+
# FTS5 syntax errors
|
|
19
|
+
if "syntax error" in error_message.lower() or "fts5" in error_message.lower():
|
|
20
|
+
clean_query = (
|
|
21
|
+
query.replace('"', "")
|
|
22
|
+
.replace("(", "")
|
|
23
|
+
.replace(")", "")
|
|
24
|
+
.replace("+", "")
|
|
25
|
+
.replace("*", "")
|
|
26
|
+
)
|
|
27
|
+
return dedent(f"""
|
|
28
|
+
# Search Failed - Invalid Syntax
|
|
29
|
+
|
|
30
|
+
The search query '{query}' contains invalid syntax that the search engine cannot process.
|
|
31
|
+
|
|
32
|
+
## Common syntax issues:
|
|
33
|
+
1. **Special characters**: Characters like `+`, `*`, `"`, `(`, `)` have special meaning in search
|
|
34
|
+
2. **Unmatched quotes**: Make sure quotes are properly paired
|
|
35
|
+
3. **Invalid operators**: Check AND, OR, NOT operators are used correctly
|
|
36
|
+
|
|
37
|
+
## How to fix:
|
|
38
|
+
1. **Simplify your search**: Try using simple words instead: `{clean_query}`
|
|
39
|
+
2. **Remove special characters**: Use alphanumeric characters and spaces
|
|
40
|
+
3. **Use basic boolean operators**: `word1 AND word2`, `word1 OR word2`, `word1 NOT word2`
|
|
41
|
+
|
|
42
|
+
## Examples of valid searches:
|
|
43
|
+
- Simple text: `project planning`
|
|
44
|
+
- Boolean AND: `project AND planning`
|
|
45
|
+
- Boolean OR: `meeting OR discussion`
|
|
46
|
+
- Boolean NOT: `project NOT archived`
|
|
47
|
+
- Grouped: `(project OR planning) AND notes`
|
|
48
|
+
|
|
49
|
+
## Try again with:
|
|
50
|
+
```
|
|
51
|
+
search_notes("INSERT_CLEAN_QUERY_HERE")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
|
|
55
|
+
""").strip()
|
|
56
|
+
|
|
57
|
+
# Project not found errors (check before general "not found")
|
|
58
|
+
if "project not found" in error_message.lower():
|
|
59
|
+
return dedent(f"""
|
|
60
|
+
# Search Failed - Project Not Found
|
|
61
|
+
|
|
62
|
+
The current project is not accessible or doesn't exist: {error_message}
|
|
63
|
+
|
|
64
|
+
## How to resolve:
|
|
65
|
+
1. **Check available projects**: `list_projects()`
|
|
66
|
+
2. **Switch to valid project**: `switch_project("valid-project-name")`
|
|
67
|
+
3. **Verify project setup**: Ensure your project is properly configured
|
|
68
|
+
|
|
69
|
+
## Current session info:
|
|
70
|
+
- Check current project: `get_current_project()`
|
|
71
|
+
- See available projects: `list_projects()`
|
|
72
|
+
""").strip()
|
|
73
|
+
|
|
74
|
+
# No results found
|
|
75
|
+
if "no results" in error_message.lower() or "not found" in error_message.lower():
|
|
76
|
+
simplified_query = (
|
|
77
|
+
" ".join(query.split()[:2])
|
|
78
|
+
if len(query.split()) > 2
|
|
79
|
+
else query.split()[0]
|
|
80
|
+
if query.split()
|
|
81
|
+
else "notes"
|
|
82
|
+
)
|
|
83
|
+
return dedent(f"""
|
|
84
|
+
# Search Complete - No Results Found
|
|
85
|
+
|
|
86
|
+
No content found matching '{query}' in the current project.
|
|
87
|
+
|
|
88
|
+
## Suggestions to try:
|
|
89
|
+
1. **Broaden your search**: Try fewer or more general terms
|
|
90
|
+
- Instead of: `{query}`
|
|
91
|
+
- Try: `{simplified_query}`
|
|
92
|
+
|
|
93
|
+
2. **Check spelling**: Verify terms are spelled correctly
|
|
94
|
+
3. **Try different search types**:
|
|
95
|
+
- Text search: `search_notes("{query}", search_type="text")`
|
|
96
|
+
- Title search: `search_notes("{query}", search_type="title")`
|
|
97
|
+
- Permalink search: `search_notes("{query}", search_type="permalink")`
|
|
98
|
+
|
|
99
|
+
4. **Use boolean operators**:
|
|
100
|
+
- Try OR search for broader results
|
|
101
|
+
|
|
102
|
+
## Check what content exists:
|
|
103
|
+
- Recent activity: `recent_activity(timeframe="7d")`
|
|
104
|
+
- List files: `list_directory("/")`
|
|
105
|
+
- Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
|
|
106
|
+
""").strip()
|
|
107
|
+
|
|
108
|
+
# Server/API errors
|
|
109
|
+
if "server error" in error_message.lower() or "internal" in error_message.lower():
|
|
110
|
+
return dedent(f"""
|
|
111
|
+
# Search Failed - Server Error
|
|
112
|
+
|
|
113
|
+
The search service encountered an error while processing '{query}': {error_message}
|
|
114
|
+
|
|
115
|
+
## Immediate steps:
|
|
116
|
+
1. **Try again**: The error might be temporary
|
|
117
|
+
2. **Simplify the query**: Use simpler search terms
|
|
118
|
+
3. **Check project status**: Ensure your project is properly synced
|
|
119
|
+
|
|
120
|
+
## Alternative approaches:
|
|
121
|
+
- Browse files directly: `list_directory("/")`
|
|
122
|
+
- Check recent activity: `recent_activity(timeframe="7d")`
|
|
123
|
+
- Try a different search type: `search_notes("{query}", search_type="title")`
|
|
124
|
+
|
|
125
|
+
## If the problem persists:
|
|
126
|
+
The search index might need to be rebuilt. Send a message to support@basicmachines.co or check the project sync status.
|
|
127
|
+
""").strip()
|
|
128
|
+
|
|
129
|
+
# Permission/access errors
|
|
130
|
+
if (
|
|
131
|
+
"permission" in error_message.lower()
|
|
132
|
+
or "access" in error_message.lower()
|
|
133
|
+
or "forbidden" in error_message.lower()
|
|
134
|
+
):
|
|
135
|
+
return f"""# Search Failed - Access Error
|
|
136
|
+
|
|
137
|
+
You don't have permission to search in the current project: {error_message}
|
|
138
|
+
|
|
139
|
+
## How to resolve:
|
|
140
|
+
1. **Check your project access**: Verify you have read permissions for this project
|
|
141
|
+
2. **Switch projects**: Try searching in a different project you have access to
|
|
142
|
+
3. **Check authentication**: You might need to re-authenticate
|
|
143
|
+
|
|
144
|
+
## Alternative actions:
|
|
145
|
+
- List available projects: `list_projects()`
|
|
146
|
+
- Switch to accessible project: `switch_project("project-name")`
|
|
147
|
+
- Check current project: `get_current_project()`"""
|
|
148
|
+
|
|
149
|
+
# Generic fallback
|
|
150
|
+
return f"""# Search Failed
|
|
151
|
+
|
|
152
|
+
Error searching for '{query}': {error_message}
|
|
153
|
+
|
|
154
|
+
## General troubleshooting:
|
|
155
|
+
1. **Check your query**: Ensure it uses valid search syntax
|
|
156
|
+
2. **Try simpler terms**: Use basic words without special characters
|
|
157
|
+
3. **Verify project access**: Make sure you can access the current project
|
|
158
|
+
4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
|
|
159
|
+
|
|
160
|
+
## Alternative approaches:
|
|
161
|
+
- Browse files: `list_directory("/")`
|
|
162
|
+
- Try different search type: `search_notes("{query}", search_type="title")`
|
|
163
|
+
- Search with filters: `search_notes("{query}", types=["entity"])`
|
|
164
|
+
|
|
165
|
+
## Need help?
|
|
166
|
+
- View recent changes: `recent_activity()`
|
|
167
|
+
- List projects: `list_projects()`
|
|
168
|
+
- Check current project: `get_current_project()`"""
|
|
169
|
+
|
|
170
|
+
|
|
13
171
|
@mcp.tool(
|
|
14
172
|
description="Search across all content in the knowledge base.",
|
|
15
173
|
)
|
|
@@ -21,7 +179,8 @@ async def search_notes(
|
|
|
21
179
|
types: Optional[List[str]] = None,
|
|
22
180
|
entity_types: Optional[List[str]] = None,
|
|
23
181
|
after_date: Optional[str] = None,
|
|
24
|
-
|
|
182
|
+
project: Optional[str] = None,
|
|
183
|
+
) -> SearchResponse | str:
|
|
25
184
|
"""Search across all content in the knowledge base.
|
|
26
185
|
|
|
27
186
|
This tool searches the knowledge base using full-text search, pattern matching,
|
|
@@ -36,6 +195,7 @@ async def search_notes(
|
|
|
36
195
|
types: Optional list of note types to search (e.g., ["note", "person"])
|
|
37
196
|
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
|
|
38
197
|
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
|
|
198
|
+
project: Optional project name to search in. If not provided, uses current active project.
|
|
39
199
|
|
|
40
200
|
Returns:
|
|
41
201
|
SearchResponse with results and pagination info
|
|
@@ -79,6 +239,9 @@ async def search_notes(
|
|
|
79
239
|
query="docs/meeting-*",
|
|
80
240
|
search_type="permalink"
|
|
81
241
|
)
|
|
242
|
+
|
|
243
|
+
# Search in specific project
|
|
244
|
+
results = await search_notes("meeting notes", project="work-project")
|
|
82
245
|
"""
|
|
83
246
|
# Create a SearchQuery object based on the parameters
|
|
84
247
|
search_query = SearchQuery()
|
|
@@ -103,11 +266,29 @@ async def search_notes(
|
|
|
103
266
|
if after_date:
|
|
104
267
|
search_query.after_date = after_date
|
|
105
268
|
|
|
269
|
+
active_project = get_active_project(project)
|
|
270
|
+
project_url = active_project.project_url
|
|
271
|
+
|
|
106
272
|
logger.info(f"Searching for {search_query}")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
response = await call_post(
|
|
276
|
+
client,
|
|
277
|
+
f"{project_url}/search/",
|
|
278
|
+
json=search_query.model_dump(),
|
|
279
|
+
params={"page": page, "page_size": page_size},
|
|
280
|
+
)
|
|
281
|
+
result = SearchResponse.model_validate(response.json())
|
|
282
|
+
|
|
283
|
+
# Check if we got no results and provide helpful guidance
|
|
284
|
+
if not result.results:
|
|
285
|
+
logger.info(f"Search returned no results for query: {query}")
|
|
286
|
+
# Don't treat this as an error, but the user might want guidance
|
|
287
|
+
# We return the empty result as normal - the user can decide if they need help
|
|
288
|
+
|
|
289
|
+
return result
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"Search failed for query '{query}': {e}")
|
|
293
|
+
# Return formatted error message as string for better user experience
|
|
294
|
+
return _format_search_error_response(str(e), query, search_type)
|