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 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
|
|
10
10
|
from basic_memory.mcp.server import mcp
|
|
11
11
|
from basic_memory.mcp.tools.utils import call_get
|
|
@@ -59,11 +59,13 @@ async def project_info(
|
|
|
59
59
|
print(f"Basic Memory version: {info.system.version}")
|
|
60
60
|
"""
|
|
61
61
|
logger.info("Getting project info")
|
|
62
|
-
project_config = await get_active_project(client, project, context)
|
|
63
|
-
project_url = project_config.permalink
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
async with get_client() as client:
|
|
64
|
+
project_config = await get_active_project(client, project, context)
|
|
65
|
+
project_url = project_config.permalink
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
# Call the API endpoint
|
|
68
|
+
response = await call_get(client, f"{project_url}/project/info")
|
|
69
|
+
|
|
70
|
+
# Convert response to ProjectInfoResponse
|
|
71
|
+
return ProjectInfoResponse.model_validate(response.json())
|
|
@@ -5,7 +5,7 @@ from typing import 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
|
|
10
10
|
from basic_memory.mcp.server import mcp
|
|
11
11
|
from basic_memory.mcp.tools.utils import call_get
|
|
@@ -102,42 +102,43 @@ async def build_context(
|
|
|
102
102
|
|
|
103
103
|
# URL is already validated and normalized by MemoryUrl type annotation
|
|
104
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
|
-
|
|
105
|
+
async with get_client() as client:
|
|
106
|
+
# Get the active project using the new stateless approach
|
|
107
|
+
active_project = await get_active_project(client, project, context)
|
|
108
|
+
|
|
109
|
+
# Check migration status and wait briefly if needed
|
|
110
|
+
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
111
|
+
|
|
112
|
+
migration_status = await wait_for_migration_or_return_status(
|
|
113
|
+
timeout=5.0, project_name=active_project.name
|
|
114
|
+
)
|
|
115
|
+
if migration_status: # pragma: no cover
|
|
116
|
+
# Return a proper GraphContext with status message
|
|
117
|
+
from basic_memory.schemas.memory import MemoryMetadata
|
|
118
|
+
from datetime import datetime
|
|
119
|
+
|
|
120
|
+
return GraphContext(
|
|
121
|
+
results=[],
|
|
122
|
+
metadata=MemoryMetadata(
|
|
123
|
+
depth=depth or 1,
|
|
124
|
+
timeframe=timeframe,
|
|
125
|
+
generated_at=datetime.now().astimezone(),
|
|
126
|
+
primary_count=0,
|
|
127
|
+
related_count=0,
|
|
128
|
+
uri=migration_status, # Include status in metadata
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
project_url = active_project.project_url
|
|
132
|
+
|
|
133
|
+
response = await call_get(
|
|
134
|
+
client,
|
|
135
|
+
f"{project_url}/memory/{memory_url_path(url)}",
|
|
136
|
+
params={
|
|
137
|
+
"depth": depth,
|
|
138
|
+
"timeframe": timeframe,
|
|
139
|
+
"page": page,
|
|
140
|
+
"page_size": page_size,
|
|
141
|
+
"max_related": max_related,
|
|
142
|
+
},
|
|
129
143
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
response = await call_get(
|
|
133
|
-
client,
|
|
134
|
-
f"{project_url}/memory/{memory_url_path(url)}",
|
|
135
|
-
params={
|
|
136
|
-
"depth": depth,
|
|
137
|
-
"timeframe": timeframe,
|
|
138
|
-
"page": page,
|
|
139
|
-
"page_size": page_size,
|
|
140
|
-
"max_related": max_related,
|
|
141
|
-
},
|
|
142
|
-
)
|
|
143
|
-
return GraphContext.model_validate(response.json())
|
|
144
|
+
return GraphContext.model_validate(response.json())
|
basic_memory/mcp/tools/canvas.py
CHANGED
|
@@ -9,7 +9,7 @@ from typing import Dict, List, Any, Optional
|
|
|
9
9
|
from loguru import logger
|
|
10
10
|
from fastmcp import Context
|
|
11
11
|
|
|
12
|
-
from basic_memory.mcp.async_client import
|
|
12
|
+
from basic_memory.mcp.async_client import get_client
|
|
13
13
|
from basic_memory.mcp.project_context import get_active_project
|
|
14
14
|
from basic_memory.mcp.server import mcp
|
|
15
15
|
from basic_memory.mcp.tools.utils import call_put
|
|
@@ -94,29 +94,30 @@ async def canvas(
|
|
|
94
94
|
Raises:
|
|
95
95
|
ToolError: If project doesn't exist or folder path is invalid
|
|
96
96
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
async with get_client() as client:
|
|
98
|
+
active_project = await get_active_project(client, project, context)
|
|
99
|
+
project_url = active_project.project_url
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
# Ensure path has .canvas extension
|
|
102
|
+
file_title = title if title.endswith(".canvas") else f"{title}.canvas"
|
|
103
|
+
file_path = f"{folder}/{file_title}"
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
# Create canvas data structure
|
|
106
|
+
canvas_data = {"nodes": nodes, "edges": edges}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
# Convert to JSON
|
|
109
|
+
canvas_json = json.dumps(canvas_data, indent=2)
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
# Write the file using the resource API
|
|
112
|
+
logger.info(f"Creating canvas file: {file_path} in project {project}")
|
|
113
|
+
response = await call_put(client, f"{project_url}/resource/{file_path}", json=canvas_json)
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
# Parse response
|
|
116
|
+
result = response.json()
|
|
117
|
+
logger.debug(result)
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
# Build summary
|
|
120
|
+
action = "Created" if response.status_code == 201 else "Updated"
|
|
121
|
+
summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
return "\n".join(summary)
|
|
@@ -14,6 +14,7 @@ from basic_memory.mcp.server import mcp
|
|
|
14
14
|
from basic_memory.mcp.tools.search import search_notes
|
|
15
15
|
from basic_memory.mcp.tools.read_note import read_note
|
|
16
16
|
from basic_memory.schemas.search import SearchResponse
|
|
17
|
+
from basic_memory.config import ConfigManager
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
|
|
@@ -90,10 +91,14 @@ async def search(
|
|
|
90
91
|
logger.info(f"ChatGPT search request: query='{query}'")
|
|
91
92
|
|
|
92
93
|
try:
|
|
94
|
+
# ChatGPT tools don't expose project parameter, so use default project
|
|
95
|
+
config = ConfigManager().config
|
|
96
|
+
default_project = config.default_project
|
|
97
|
+
|
|
93
98
|
# Call underlying search_notes with sensible defaults for ChatGPT
|
|
94
99
|
results = await search_notes.fn(
|
|
95
100
|
query=query,
|
|
96
|
-
project=
|
|
101
|
+
project=default_project, # Use default project for ChatGPT
|
|
97
102
|
page=1,
|
|
98
103
|
page_size=10, # Reasonable default for ChatGPT consumption
|
|
99
104
|
search_type="text", # Default to full-text search
|
|
@@ -149,10 +154,14 @@ async def fetch(
|
|
|
149
154
|
logger.info(f"ChatGPT fetch request: id='{id}'")
|
|
150
155
|
|
|
151
156
|
try:
|
|
157
|
+
# ChatGPT tools don't expose project parameter, so use default project
|
|
158
|
+
config = ConfigManager().config
|
|
159
|
+
default_project = config.default_project
|
|
160
|
+
|
|
152
161
|
# Call underlying read_note function
|
|
153
162
|
content = await read_note.fn(
|
|
154
163
|
identifier=id,
|
|
155
|
-
project=
|
|
164
|
+
project=default_project, # Use default project for ChatGPT
|
|
156
165
|
page=1,
|
|
157
166
|
page_size=10, # Default pagination
|
|
158
167
|
context=context,
|
|
@@ -7,7 +7,7 @@ from fastmcp import Context
|
|
|
7
7
|
from basic_memory.mcp.project_context import get_active_project
|
|
8
8
|
from basic_memory.mcp.tools.utils import call_delete
|
|
9
9
|
from basic_memory.mcp.server import mcp
|
|
10
|
-
from basic_memory.mcp.async_client import
|
|
10
|
+
from basic_memory.mcp.async_client import get_client
|
|
11
11
|
from basic_memory.schemas import DeleteEntitiesResponse
|
|
12
12
|
|
|
13
13
|
|
|
@@ -202,23 +202,24 @@ async def delete_note(
|
|
|
202
202
|
with suggestions for finding the correct identifier, including search
|
|
203
203
|
commands and alternative formats to try.
|
|
204
204
|
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
205
|
+
async with get_client() as client:
|
|
206
|
+
active_project = await get_active_project(client, project, context)
|
|
207
|
+
project_url = active_project.project_url
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
|
|
211
|
+
result = DeleteEntitiesResponse.model_validate(response.json())
|
|
212
|
+
|
|
213
|
+
if result.deleted:
|
|
214
|
+
logger.info(
|
|
215
|
+
f"Successfully deleted note: {identifier} in project: {active_project.name}"
|
|
216
|
+
)
|
|
217
|
+
return True
|
|
218
|
+
else:
|
|
219
|
+
logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
except Exception as e: # pragma: no cover
|
|
223
|
+
logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
|
|
224
|
+
# Return formatted error message for better user experience
|
|
225
|
+
return _format_delete_error_response(active_project.name, str(e), identifier)
|
|
@@ -5,7 +5,7 @@ from typing import 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, add_project_metadata
|
|
10
10
|
from basic_memory.mcp.server import mcp
|
|
11
11
|
from basic_memory.mcp.tools.utils import call_patch
|
|
@@ -214,106 +214,107 @@ async def edit_note(
|
|
|
214
214
|
search_notes() first to find the correct identifier. The tool provides detailed
|
|
215
215
|
error messages with suggestions if operations fail.
|
|
216
216
|
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
217
|
+
async with get_client() as client:
|
|
218
|
+
active_project = await get_active_project(client, project, context)
|
|
219
|
+
project_url = active_project.project_url
|
|
220
|
+
|
|
221
|
+
logger.info("MCP tool call", tool="edit_note", identifier=identifier, operation=operation)
|
|
222
|
+
|
|
223
|
+
# Validate operation
|
|
224
|
+
valid_operations = ["append", "prepend", "find_replace", "replace_section"]
|
|
225
|
+
if operation not in valid_operations:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Invalid operation '{operation}'. Must be one of: {', '.join(valid_operations)}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Validate required parameters for specific operations
|
|
231
|
+
if operation == "find_replace" and not find_text:
|
|
232
|
+
raise ValueError("find_text parameter is required for find_replace operation")
|
|
233
|
+
if operation == "replace_section" and not section:
|
|
234
|
+
raise ValueError("section parameter is required for replace_section operation")
|
|
235
|
+
|
|
236
|
+
# Use the PATCH endpoint to edit the entity
|
|
237
|
+
try:
|
|
238
|
+
# Prepare the edit request data
|
|
239
|
+
edit_data = {
|
|
240
|
+
"operation": operation,
|
|
241
|
+
"content": content,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Add optional parameters
|
|
245
|
+
if section:
|
|
246
|
+
edit_data["section"] = section
|
|
247
|
+
if find_text:
|
|
248
|
+
edit_data["find_text"] = find_text
|
|
249
|
+
if expected_replacements != 1: # Only send if different from default
|
|
250
|
+
edit_data["expected_replacements"] = str(expected_replacements)
|
|
251
|
+
|
|
252
|
+
# Call the PATCH endpoint
|
|
253
|
+
url = f"{project_url}/knowledge/entities/{identifier}"
|
|
254
|
+
response = await call_patch(client, url, json=edit_data)
|
|
255
|
+
result = EntityResponse.model_validate(response.json())
|
|
256
|
+
|
|
257
|
+
# Format summary
|
|
258
|
+
summary = [
|
|
259
|
+
f"# Edited note ({operation})",
|
|
260
|
+
f"project: {active_project.name}",
|
|
261
|
+
f"file_path: {result.file_path}",
|
|
262
|
+
f"permalink: {result.permalink}",
|
|
263
|
+
f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
# Add operation-specific details
|
|
267
|
+
if operation == "append":
|
|
268
|
+
lines_added = len(content.split("\n"))
|
|
269
|
+
summary.append(f"operation: Added {lines_added} lines to end of note")
|
|
270
|
+
elif operation == "prepend":
|
|
271
|
+
lines_added = len(content.split("\n"))
|
|
272
|
+
summary.append(f"operation: Added {lines_added} lines to beginning of note")
|
|
273
|
+
elif operation == "find_replace":
|
|
274
|
+
# For find_replace, we can't easily count replacements from here
|
|
275
|
+
# since we don't have the original content, but the server handled it
|
|
276
|
+
summary.append("operation: Find and replace operation completed")
|
|
277
|
+
elif operation == "replace_section":
|
|
278
|
+
summary.append(f"operation: Replaced content under section '{section}'")
|
|
279
|
+
|
|
280
|
+
# Count observations by category (reuse logic from write_note)
|
|
281
|
+
categories = {}
|
|
282
|
+
if result.observations:
|
|
283
|
+
for obs in result.observations:
|
|
284
|
+
categories[obs.category] = categories.get(obs.category, 0) + 1
|
|
285
|
+
|
|
286
|
+
summary.append("\\n## Observations")
|
|
287
|
+
for category, count in sorted(categories.items()):
|
|
288
|
+
summary.append(f"- {category}: {count}")
|
|
289
|
+
|
|
290
|
+
# Count resolved/unresolved relations
|
|
291
|
+
unresolved = 0
|
|
292
|
+
resolved = 0
|
|
293
|
+
if result.relations:
|
|
294
|
+
unresolved = sum(1 for r in result.relations if not r.to_id)
|
|
295
|
+
resolved = len(result.relations) - unresolved
|
|
296
|
+
|
|
297
|
+
summary.append("\\n## Relations")
|
|
298
|
+
summary.append(f"- Resolved: {resolved}")
|
|
299
|
+
if unresolved:
|
|
300
|
+
summary.append(f"- Unresolved: {unresolved}")
|
|
301
|
+
|
|
302
|
+
logger.info(
|
|
303
|
+
"MCP tool response",
|
|
304
|
+
tool="edit_note",
|
|
305
|
+
operation=operation,
|
|
306
|
+
project=active_project.name,
|
|
307
|
+
permalink=result.permalink,
|
|
308
|
+
observations_count=len(result.observations),
|
|
309
|
+
relations_count=len(result.relations),
|
|
310
|
+
status_code=response.status_code,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
result = "\n".join(summary)
|
|
314
|
+
return add_project_metadata(result, active_project.name)
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.error(f"Error editing note: {e}")
|
|
318
|
+
return _format_error_response(
|
|
319
|
+
str(e), operation, identifier, find_text, expected_replacements, active_project.name
|
|
320
|
+
)
|