basic-memory 0.15.0__py3-none-any.whl → 0.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +1 -1
- basic_memory/api/routers/directory_router.py +23 -2
- basic_memory/api/routers/project_router.py +1 -0
- basic_memory/cli/auth.py +2 -2
- basic_memory/cli/commands/command_utils.py +11 -28
- basic_memory/cli/commands/mcp.py +72 -67
- basic_memory/cli/commands/project.py +54 -49
- basic_memory/cli/commands/status.py +6 -15
- basic_memory/config.py +55 -9
- basic_memory/deps.py +7 -5
- basic_memory/ignore_utils.py +7 -7
- basic_memory/mcp/async_client.py +102 -4
- basic_memory/mcp/prompts/continue_conversation.py +16 -15
- basic_memory/mcp/prompts/search.py +12 -11
- basic_memory/mcp/resources/ai_assistant_guide.md +185 -453
- basic_memory/mcp/resources/project_info.py +9 -7
- basic_memory/mcp/tools/build_context.py +40 -39
- basic_memory/mcp/tools/canvas.py +21 -20
- basic_memory/mcp/tools/chatgpt_tools.py +11 -2
- basic_memory/mcp/tools/delete_note.py +22 -21
- basic_memory/mcp/tools/edit_note.py +105 -104
- basic_memory/mcp/tools/list_directory.py +98 -95
- basic_memory/mcp/tools/move_note.py +127 -125
- basic_memory/mcp/tools/project_management.py +101 -98
- basic_memory/mcp/tools/read_content.py +64 -63
- basic_memory/mcp/tools/read_note.py +88 -88
- basic_memory/mcp/tools/recent_activity.py +139 -135
- basic_memory/mcp/tools/search.py +27 -26
- basic_memory/mcp/tools/sync_status.py +133 -128
- basic_memory/mcp/tools/utils.py +0 -15
- basic_memory/mcp/tools/view_note.py +14 -28
- basic_memory/mcp/tools/write_note.py +97 -87
- basic_memory/repository/entity_repository.py +60 -0
- basic_memory/repository/repository.py +16 -3
- basic_memory/repository/search_repository.py +42 -0
- basic_memory/schemas/project_info.py +1 -1
- basic_memory/services/directory_service.py +124 -3
- basic_memory/services/entity_service.py +31 -9
- basic_memory/services/project_service.py +97 -10
- basic_memory/services/search_service.py +16 -8
- basic_memory/sync/sync_service.py +28 -13
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/METADATA +51 -4
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/RECORD +46 -47
- basic_memory/mcp/tools/headers.py +0 -44
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.15.0.dist-info → basic_memory-0.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,7 @@ from typing import List, Union, Optional
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
from fastmcp import Context
|
|
7
7
|
|
|
8
|
-
from basic_memory.mcp.async_client import
|
|
8
|
+
from basic_memory.mcp.async_client import get_client
|
|
9
9
|
from basic_memory.mcp.project_context import get_active_project, resolve_project_parameter
|
|
10
10
|
from basic_memory.mcp.server import mcp
|
|
11
11
|
from basic_memory.mcp.tools.utils import call_get
|
|
@@ -98,162 +98,166 @@ async def recent_activity(
|
|
|
98
98
|
- For focused queries, consider using build_context with a specific URI
|
|
99
99
|
- Max timeframe is 1 year in the past
|
|
100
100
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
101
|
+
async with get_client() as client:
|
|
102
|
+
# Build common parameters for API calls
|
|
103
|
+
params = {
|
|
104
|
+
"page": 1,
|
|
105
|
+
"page_size": 10,
|
|
106
|
+
"max_related": 10,
|
|
107
|
+
}
|
|
108
|
+
if depth:
|
|
109
|
+
params["depth"] = depth
|
|
110
|
+
if timeframe:
|
|
111
|
+
params["timeframe"] = timeframe # pyright: ignore
|
|
112
|
+
|
|
113
|
+
# Validate and convert type parameter
|
|
114
|
+
if type:
|
|
115
|
+
# Convert single string to list
|
|
116
|
+
if isinstance(type, str):
|
|
117
|
+
type_list = [type]
|
|
118
|
+
else:
|
|
119
|
+
type_list = type
|
|
120
|
+
|
|
121
|
+
# Validate each type against SearchItemType enum
|
|
122
|
+
validated_types = []
|
|
123
|
+
for t in type_list:
|
|
124
|
+
try:
|
|
125
|
+
# Try to convert string to enum
|
|
126
|
+
if isinstance(t, str):
|
|
127
|
+
validated_types.append(SearchItemType(t.lower()))
|
|
128
|
+
except ValueError:
|
|
129
|
+
valid_types = [t.value for t in SearchItemType]
|
|
130
|
+
raise ValueError(f"Invalid type: {t}. Valid types are: {valid_types}")
|
|
131
|
+
|
|
132
|
+
# Add validated types to params
|
|
133
|
+
params["type"] = [t.value for t in validated_types] # pyright: ignore
|
|
134
|
+
|
|
135
|
+
# Resolve project parameter using the three-tier hierarchy
|
|
136
|
+
resolved_project = await resolve_project_parameter(project)
|
|
137
|
+
|
|
138
|
+
if resolved_project is None:
|
|
139
|
+
# Discovery Mode: Get activity across all projects
|
|
140
|
+
logger.info(
|
|
141
|
+
f"Getting recent activity across all projects: type={type}, depth={depth}, timeframe={timeframe}"
|
|
142
|
+
)
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
144
|
+
# Get list of all projects
|
|
145
|
+
response = await call_get(client, "/projects/projects")
|
|
146
|
+
project_list = ProjectList.model_validate(response.json())
|
|
147
|
+
|
|
148
|
+
projects_activity = {}
|
|
149
|
+
total_items = 0
|
|
150
|
+
total_entities = 0
|
|
151
|
+
total_relations = 0
|
|
152
|
+
total_observations = 0
|
|
153
|
+
most_active_project = None
|
|
154
|
+
most_active_count = 0
|
|
155
|
+
active_projects = 0
|
|
156
|
+
|
|
157
|
+
# Query each project's activity
|
|
158
|
+
for project_info in project_list.projects:
|
|
159
|
+
project_activity = await _get_project_activity(client, project_info, params, depth)
|
|
160
|
+
projects_activity[project_info.name] = project_activity
|
|
161
|
+
|
|
162
|
+
# Aggregate stats
|
|
163
|
+
item_count = project_activity.item_count
|
|
164
|
+
if item_count > 0:
|
|
165
|
+
active_projects += 1
|
|
166
|
+
total_items += item_count
|
|
167
|
+
|
|
168
|
+
# Count by type
|
|
169
|
+
for result in project_activity.activity.results:
|
|
170
|
+
if result.primary_result.type == "entity":
|
|
171
|
+
total_entities += 1
|
|
172
|
+
elif result.primary_result.type == "relation":
|
|
173
|
+
total_relations += 1
|
|
174
|
+
elif result.primary_result.type == "observation":
|
|
175
|
+
total_observations += 1
|
|
176
|
+
|
|
177
|
+
# Track most active project
|
|
178
|
+
if item_count > most_active_count:
|
|
179
|
+
most_active_count = item_count
|
|
180
|
+
most_active_project = project_info.name
|
|
181
|
+
|
|
182
|
+
# Build summary stats
|
|
183
|
+
summary = ActivityStats(
|
|
184
|
+
total_projects=len(project_list.projects),
|
|
185
|
+
active_projects=active_projects,
|
|
186
|
+
most_active_project=most_active_project,
|
|
187
|
+
total_items=total_items,
|
|
188
|
+
total_entities=total_entities,
|
|
189
|
+
total_relations=total_relations,
|
|
190
|
+
total_observations=total_observations,
|
|
191
|
+
)
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
# Generate guidance for the assistant
|
|
194
|
+
guidance_lines = ["\n" + "─" * 40]
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
guidance_lines.extend(
|
|
197
|
-
[
|
|
198
|
-
f"Suggested project: '{most_active_project}' (most active with {most_active_count} items)",
|
|
199
|
-
f"Ask user: 'Should I use {most_active_project} for this task, or would you prefer a different project?'",
|
|
200
|
-
]
|
|
201
|
-
)
|
|
202
|
-
elif active_projects > 0:
|
|
203
|
-
# Has activity but no clear most active project
|
|
204
|
-
active_project_names = [
|
|
205
|
-
name for name, activity in projects_activity.items() if activity.item_count > 0
|
|
206
|
-
]
|
|
207
|
-
if len(active_project_names) == 1:
|
|
196
|
+
if most_active_project and most_active_count > 0:
|
|
208
197
|
guidance_lines.extend(
|
|
209
198
|
[
|
|
210
|
-
f"Suggested project: '{
|
|
211
|
-
f"Ask user: 'Should I use {
|
|
199
|
+
f"Suggested project: '{most_active_project}' (most active with {most_active_count} items)",
|
|
200
|
+
f"Ask user: 'Should I use {most_active_project} for this task, or would you prefer a different project?'",
|
|
212
201
|
]
|
|
213
202
|
)
|
|
203
|
+
elif active_projects > 0:
|
|
204
|
+
# Has activity but no clear most active project
|
|
205
|
+
active_project_names = [
|
|
206
|
+
name for name, activity in projects_activity.items() if activity.item_count > 0
|
|
207
|
+
]
|
|
208
|
+
if len(active_project_names) == 1:
|
|
209
|
+
guidance_lines.extend(
|
|
210
|
+
[
|
|
211
|
+
f"Suggested project: '{active_project_names[0]}' (only active project)",
|
|
212
|
+
f"Ask user: 'Should I use {active_project_names[0]} for this task?'",
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
guidance_lines.extend(
|
|
217
|
+
[
|
|
218
|
+
f"Multiple active projects found: {', '.join(active_project_names)}",
|
|
219
|
+
"Ask user: 'Which project should I use for this task?'",
|
|
220
|
+
]
|
|
221
|
+
)
|
|
214
222
|
else:
|
|
223
|
+
# No recent activity
|
|
215
224
|
guidance_lines.extend(
|
|
216
225
|
[
|
|
217
|
-
|
|
218
|
-
"Ask
|
|
226
|
+
"No recent activity found in any project.",
|
|
227
|
+
"Consider: Ask which project to use or if they want to create a new one.",
|
|
219
228
|
]
|
|
220
229
|
)
|
|
221
|
-
|
|
222
|
-
# No recent activity
|
|
230
|
+
|
|
223
231
|
guidance_lines.extend(
|
|
224
232
|
[
|
|
225
|
-
"
|
|
226
|
-
"
|
|
233
|
+
"",
|
|
234
|
+
"Session reminder: Remember their project choice throughout this conversation.",
|
|
227
235
|
]
|
|
228
236
|
)
|
|
229
237
|
|
|
230
|
-
|
|
231
|
-
["", "Session reminder: Remember their project choice throughout this conversation."]
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
guidance = "\n".join(guidance_lines)
|
|
238
|
+
guidance = "\n".join(guidance_lines)
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
# Format discovery mode output
|
|
241
|
+
return _format_discovery_output(projects_activity, summary, timeframe, guidance)
|
|
238
242
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
else:
|
|
244
|
+
# Project-Specific Mode: Get activity for specific project
|
|
245
|
+
logger.info(
|
|
246
|
+
f"Getting recent activity from project {resolved_project}: type={type}, depth={depth}, timeframe={timeframe}"
|
|
247
|
+
)
|
|
244
248
|
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
active_project = await get_active_project(client, resolved_project, context)
|
|
250
|
+
project_url = active_project.project_url
|
|
247
251
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
response = await call_get(
|
|
253
|
+
client,
|
|
254
|
+
f"{project_url}/memory/recent",
|
|
255
|
+
params=params,
|
|
256
|
+
)
|
|
257
|
+
activity_data = GraphContext.model_validate(response.json())
|
|
254
258
|
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
# Format project-specific mode output
|
|
260
|
+
return _format_project_output(resolved_project, activity_data, timeframe, type)
|
|
257
261
|
|
|
258
262
|
|
|
259
263
|
async def _get_project_activity(
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import List, Optional
|
|
|
6
6
|
from loguru import logger
|
|
7
7
|
from fastmcp import Context
|
|
8
8
|
|
|
9
|
-
from basic_memory.mcp.async_client import
|
|
9
|
+
from basic_memory.mcp.async_client import get_client
|
|
10
10
|
from basic_memory.mcp.project_context import get_active_project
|
|
11
11
|
from basic_memory.mcp.server import mcp
|
|
12
12
|
from basic_memory.mcp.tools.utils import call_post
|
|
@@ -353,31 +353,32 @@ async def search_notes(
|
|
|
353
353
|
if after_date:
|
|
354
354
|
search_query.after_date = after_date
|
|
355
355
|
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
async with get_client() as client:
|
|
357
|
+
active_project = await get_active_project(client, project, context)
|
|
358
|
+
project_url = active_project.project_url
|
|
358
359
|
|
|
359
|
-
|
|
360
|
+
logger.info(f"Searching for {search_query} in project {active_project.name}")
|
|
360
361
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)
|
|
368
|
-
result = SearchResponse.model_validate(response.json())
|
|
369
|
-
|
|
370
|
-
# Check if we got no results and provide helpful guidance
|
|
371
|
-
if not result.results:
|
|
372
|
-
logger.info(
|
|
373
|
-
f"Search returned no results for query: {query} in project {active_project.name}"
|
|
362
|
+
try:
|
|
363
|
+
response = await call_post(
|
|
364
|
+
client,
|
|
365
|
+
f"{project_url}/search/",
|
|
366
|
+
json=search_query.model_dump(),
|
|
367
|
+
params={"page": page, "page_size": page_size},
|
|
374
368
|
)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
369
|
+
result = SearchResponse.model_validate(response.json())
|
|
370
|
+
|
|
371
|
+
# Check if we got no results and provide helpful guidance
|
|
372
|
+
if not result.results:
|
|
373
|
+
logger.info(
|
|
374
|
+
f"Search returned no results for query: {query} in project {active_project.name}"
|
|
375
|
+
)
|
|
376
|
+
# Don't treat this as an error, but the user might want guidance
|
|
377
|
+
# We return the empty result as normal - the user can decide if they need help
|
|
378
|
+
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Search failed for query '{query}': {e}, project: {active_project.name}")
|
|
383
|
+
# Return formatted error message as string for better user experience
|
|
384
|
+
return _format_search_error_response(active_project.name, str(e), query, search_type)
|