basic-memory 0.2.12__py3-none-any.whl → 0.16.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 +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +27 -3
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- 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/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/api/app.py +63 -31
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +165 -28
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +28 -67
- basic_memory/api/routers/project_router.py +406 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +219 -14
- basic_memory/api/routers/search_router.py +21 -13
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +52 -1
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +13 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +51 -0
- basic_memory/cli/commands/db.py +26 -7
- basic_memory/cli/commands/import_chatgpt.py +83 -0
- basic_memory/cli/commands/import_claude_conversations.py +86 -0
- basic_memory/cli/commands/import_claude_projects.py +85 -0
- basic_memory/cli/commands/import_memory_json.py +35 -92
- basic_memory/cli/commands/mcp.py +84 -10
- basic_memory/cli/commands/project.py +876 -0
- basic_memory/cli/commands/status.py +47 -30
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +13 -6
- basic_memory/config.py +481 -22
- basic_memory/db.py +192 -32
- basic_memory/deps.py +252 -22
- basic_memory/file_utils.py +113 -58
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +177 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +143 -23
- basic_memory/markdown/markdown_processor.py +3 -3
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +28 -13
- basic_memory/mcp/async_client.py +134 -4
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +7 -13
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +130 -0
- basic_memory/mcp/tools/chatgpt_tools.py +187 -0
- basic_memory/mcp/tools/delete_note.py +225 -0
- basic_memory/mcp/tools/edit_note.py +320 -0
- basic_memory/mcp/tools/list_directory.py +167 -0
- basic_memory/mcp/tools/move_note.py +545 -0
- basic_memory/mcp/tools/project_management.py +200 -0
- basic_memory/mcp/tools/read_content.py +271 -0
- basic_memory/mcp/tools/read_note.py +255 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +369 -14
- basic_memory/mcp/tools/utils.py +374 -16
- basic_memory/mcp/tools/view_note.py +77 -0
- basic_memory/mcp/tools/write_note.py +207 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +67 -15
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +10 -6
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +229 -7
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +103 -0
- basic_memory/repository/relation_repository.py +21 -2
- basic_memory/repository/repository.py +147 -29
- basic_memory/repository/search_repository.py +437 -59
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +97 -8
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +188 -23
- basic_memory/schemas/project_info.py +211 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +57 -3
- basic_memory/schemas/response.py +9 -1
- basic_memory/schemas/search.py +33 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +251 -106
- basic_memory/services/directory_service.py +295 -0
- basic_memory/services/entity_service.py +595 -60
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +284 -30
- basic_memory/services/initialization.py +191 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +863 -0
- basic_memory/services/search_service.py +172 -34
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1176 -96
- basic_memory/sync/watch_service.py +412 -135
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +388 -28
- basic_memory-0.16.1.dist-info/METADATA +493 -0
- basic_memory-0.16.1.dist-info/RECORD +148 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -203
- basic_memory/mcp/tools/knowledge.py +0 -56
- basic_memory/mcp/tools/memory.py +0 -151
- basic_memory/mcp/tools/notes.py +0 -122
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -34
- basic_memory-0.2.12.dist-info/METADATA +0 -291
- basic_memory-0.2.12.dist-info/RECORD +0 -78
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.2.12.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""ChatGPT import service for Basic Memory."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
|
|
8
|
+
from basic_memory.importers.base import Importer
|
|
9
|
+
from basic_memory.schemas.importer import ChatImportResult
|
|
10
|
+
from basic_memory.importers.utils import clean_filename, format_timestamp
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChatGPTImporter(Importer[ChatImportResult]):
|
|
16
|
+
"""Service for importing ChatGPT conversations."""
|
|
17
|
+
|
|
18
|
+
async def import_data(
|
|
19
|
+
self, source_data, destination_folder: str, **kwargs: Any
|
|
20
|
+
) -> ChatImportResult:
|
|
21
|
+
"""Import conversations from ChatGPT JSON export.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
source_path: Path to the ChatGPT conversations.json file.
|
|
25
|
+
destination_folder: Destination folder within the project.
|
|
26
|
+
**kwargs: Additional keyword arguments.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ChatImportResult containing statistics and status of the import.
|
|
30
|
+
"""
|
|
31
|
+
try: # pragma: no cover
|
|
32
|
+
# Ensure the destination folder exists
|
|
33
|
+
self.ensure_folder_exists(destination_folder)
|
|
34
|
+
conversations = source_data
|
|
35
|
+
|
|
36
|
+
# Process each conversation
|
|
37
|
+
messages_imported = 0
|
|
38
|
+
chats_imported = 0
|
|
39
|
+
|
|
40
|
+
for chat in conversations:
|
|
41
|
+
# Convert to entity
|
|
42
|
+
entity = self._format_chat_content(destination_folder, chat)
|
|
43
|
+
|
|
44
|
+
# Write file
|
|
45
|
+
file_path = self.base_path / f"{entity.frontmatter.metadata['permalink']}.md"
|
|
46
|
+
await self.write_entity(entity, file_path)
|
|
47
|
+
|
|
48
|
+
# Count messages
|
|
49
|
+
msg_count = sum(
|
|
50
|
+
1
|
|
51
|
+
for node in chat["mapping"].values()
|
|
52
|
+
if node.get("message")
|
|
53
|
+
and not node.get("message", {})
|
|
54
|
+
.get("metadata", {})
|
|
55
|
+
.get("is_visually_hidden_from_conversation")
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
chats_imported += 1
|
|
59
|
+
messages_imported += msg_count
|
|
60
|
+
|
|
61
|
+
return ChatImportResult(
|
|
62
|
+
import_count={"conversations": chats_imported, "messages": messages_imported},
|
|
63
|
+
success=True,
|
|
64
|
+
conversations=chats_imported,
|
|
65
|
+
messages=messages_imported,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
except Exception as e: # pragma: no cover
|
|
69
|
+
logger.exception("Failed to import ChatGPT conversations")
|
|
70
|
+
return self.handle_error("Failed to import ChatGPT conversations", e) # pyright: ignore [reportReturnType]
|
|
71
|
+
|
|
72
|
+
def _format_chat_content(
|
|
73
|
+
self, folder: str, conversation: Dict[str, Any]
|
|
74
|
+
) -> EntityMarkdown: # pragma: no cover
|
|
75
|
+
"""Convert chat conversation to Basic Memory entity.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
folder: Destination folder name.
|
|
79
|
+
conversation: ChatGPT conversation data.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
EntityMarkdown instance representing the conversation.
|
|
83
|
+
"""
|
|
84
|
+
# Extract timestamps
|
|
85
|
+
created_at = conversation["create_time"]
|
|
86
|
+
modified_at = conversation["update_time"]
|
|
87
|
+
|
|
88
|
+
root_id = None
|
|
89
|
+
# Find root message
|
|
90
|
+
for node_id, node in conversation["mapping"].items():
|
|
91
|
+
if node.get("parent") is None:
|
|
92
|
+
root_id = node_id
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
# Generate permalink
|
|
96
|
+
date_prefix = datetime.fromtimestamp(created_at).astimezone().strftime("%Y%m%d")
|
|
97
|
+
clean_title = clean_filename(conversation["title"])
|
|
98
|
+
|
|
99
|
+
# Format content
|
|
100
|
+
content = self._format_chat_markdown(
|
|
101
|
+
title=conversation["title"],
|
|
102
|
+
mapping=conversation["mapping"],
|
|
103
|
+
root_id=root_id,
|
|
104
|
+
created_at=created_at,
|
|
105
|
+
modified_at=modified_at,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Create entity
|
|
109
|
+
entity = EntityMarkdown(
|
|
110
|
+
frontmatter=EntityFrontmatter(
|
|
111
|
+
metadata={
|
|
112
|
+
"type": "conversation",
|
|
113
|
+
"title": conversation["title"],
|
|
114
|
+
"created": format_timestamp(created_at),
|
|
115
|
+
"modified": format_timestamp(modified_at),
|
|
116
|
+
"permalink": f"{folder}/{date_prefix}-{clean_title}",
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
content=content,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return entity
|
|
123
|
+
|
|
124
|
+
def _format_chat_markdown(
|
|
125
|
+
self,
|
|
126
|
+
title: str,
|
|
127
|
+
mapping: Dict[str, Any],
|
|
128
|
+
root_id: Optional[str],
|
|
129
|
+
created_at: float,
|
|
130
|
+
modified_at: float,
|
|
131
|
+
) -> str: # pragma: no cover
|
|
132
|
+
"""Format chat as clean markdown.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
title: Chat title.
|
|
136
|
+
mapping: Message mapping.
|
|
137
|
+
root_id: Root message ID.
|
|
138
|
+
created_at: Creation timestamp.
|
|
139
|
+
modified_at: Modification timestamp.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Formatted markdown content.
|
|
143
|
+
"""
|
|
144
|
+
# Start with title
|
|
145
|
+
lines = [f"# {title}\n"]
|
|
146
|
+
|
|
147
|
+
# Traverse message tree
|
|
148
|
+
seen_msgs: Set[str] = set()
|
|
149
|
+
messages = self._traverse_messages(mapping, root_id, seen_msgs)
|
|
150
|
+
|
|
151
|
+
# Format each message
|
|
152
|
+
for msg in messages:
|
|
153
|
+
# Skip hidden messages
|
|
154
|
+
if msg.get("metadata", {}).get("is_visually_hidden_from_conversation"):
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
# Get author and timestamp
|
|
158
|
+
author = msg["author"]["role"].title()
|
|
159
|
+
ts = format_timestamp(msg["create_time"]) if msg.get("create_time") else ""
|
|
160
|
+
|
|
161
|
+
# Add message header
|
|
162
|
+
lines.append(f"### {author} ({ts})")
|
|
163
|
+
|
|
164
|
+
# Add message content
|
|
165
|
+
content = self._get_message_content(msg)
|
|
166
|
+
if content:
|
|
167
|
+
lines.append(content)
|
|
168
|
+
|
|
169
|
+
# Add spacing
|
|
170
|
+
lines.append("")
|
|
171
|
+
|
|
172
|
+
return "\n".join(lines)
|
|
173
|
+
|
|
174
|
+
def _get_message_content(self, message: Dict[str, Any]) -> str: # pragma: no cover
|
|
175
|
+
"""Extract clean message content.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
message: Message data.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Cleaned message content.
|
|
182
|
+
"""
|
|
183
|
+
if not message or "content" not in message:
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
content = message["content"]
|
|
187
|
+
if content.get("content_type") == "text":
|
|
188
|
+
return "\n".join(content.get("parts", []))
|
|
189
|
+
elif content.get("content_type") == "code":
|
|
190
|
+
return f"```{content.get('language', '')}\n{content.get('text', '')}\n```"
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
def _traverse_messages(
|
|
194
|
+
self, mapping: Dict[str, Any], root_id: Optional[str], seen: Set[str]
|
|
195
|
+
) -> List[Dict[str, Any]]: # pragma: no cover
|
|
196
|
+
"""Traverse message tree iteratively to handle deep conversations.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
mapping: Message mapping.
|
|
200
|
+
root_id: Root message ID.
|
|
201
|
+
seen: Set of seen message IDs.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
List of message data.
|
|
205
|
+
"""
|
|
206
|
+
messages = []
|
|
207
|
+
if not root_id:
|
|
208
|
+
return messages
|
|
209
|
+
|
|
210
|
+
# Use iterative approach with stack to avoid recursion depth issues
|
|
211
|
+
stack = [root_id]
|
|
212
|
+
|
|
213
|
+
while stack:
|
|
214
|
+
node_id = stack.pop()
|
|
215
|
+
if not node_id:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
node = mapping.get(node_id)
|
|
219
|
+
if not node:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Process current node if it has a message and hasn't been seen
|
|
223
|
+
if node["id"] not in seen and node.get("message"):
|
|
224
|
+
seen.add(node["id"])
|
|
225
|
+
messages.append(node["message"])
|
|
226
|
+
|
|
227
|
+
# Add children to stack in reverse order to maintain conversation flow
|
|
228
|
+
children = node.get("children", [])
|
|
229
|
+
for child_id in reversed(children):
|
|
230
|
+
stack.append(child_id)
|
|
231
|
+
|
|
232
|
+
return messages
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Claude conversations import service for Basic Memory."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
|
|
9
|
+
from basic_memory.importers.base import Importer
|
|
10
|
+
from basic_memory.schemas.importer import ChatImportResult
|
|
11
|
+
from basic_memory.importers.utils import clean_filename, format_timestamp
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClaudeConversationsImporter(Importer[ChatImportResult]):
|
|
17
|
+
"""Service for importing Claude conversations."""
|
|
18
|
+
|
|
19
|
+
async def import_data(
|
|
20
|
+
self, source_data, destination_folder: str, **kwargs: Any
|
|
21
|
+
) -> ChatImportResult:
|
|
22
|
+
"""Import conversations from Claude JSON export.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
source_data: Path to the Claude conversations.json file.
|
|
26
|
+
destination_folder: Destination folder within the project.
|
|
27
|
+
**kwargs: Additional keyword arguments.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
ChatImportResult containing statistics and status of the import.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
# Ensure the destination folder exists
|
|
34
|
+
folder_path = self.ensure_folder_exists(destination_folder)
|
|
35
|
+
|
|
36
|
+
conversations = source_data
|
|
37
|
+
|
|
38
|
+
# Process each conversation
|
|
39
|
+
messages_imported = 0
|
|
40
|
+
chats_imported = 0
|
|
41
|
+
|
|
42
|
+
for chat in conversations:
|
|
43
|
+
# Convert to entity
|
|
44
|
+
entity = self._format_chat_content(
|
|
45
|
+
base_path=folder_path,
|
|
46
|
+
name=chat["name"],
|
|
47
|
+
messages=chat["chat_messages"],
|
|
48
|
+
created_at=chat["created_at"],
|
|
49
|
+
modified_at=chat["updated_at"],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Write file
|
|
53
|
+
file_path = self.base_path / Path(f"{entity.frontmatter.metadata['permalink']}.md")
|
|
54
|
+
await self.write_entity(entity, file_path)
|
|
55
|
+
|
|
56
|
+
chats_imported += 1
|
|
57
|
+
messages_imported += len(chat["chat_messages"])
|
|
58
|
+
|
|
59
|
+
return ChatImportResult(
|
|
60
|
+
import_count={"conversations": chats_imported, "messages": messages_imported},
|
|
61
|
+
success=True,
|
|
62
|
+
conversations=chats_imported,
|
|
63
|
+
messages=messages_imported,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except Exception as e: # pragma: no cover
|
|
67
|
+
logger.exception("Failed to import Claude conversations")
|
|
68
|
+
return self.handle_error("Failed to import Claude conversations", e) # pyright: ignore [reportReturnType]
|
|
69
|
+
|
|
70
|
+
def _format_chat_content(
|
|
71
|
+
self,
|
|
72
|
+
base_path: Path,
|
|
73
|
+
name: str,
|
|
74
|
+
messages: List[Dict[str, Any]],
|
|
75
|
+
created_at: str,
|
|
76
|
+
modified_at: str,
|
|
77
|
+
) -> EntityMarkdown:
|
|
78
|
+
"""Convert chat messages to Basic Memory entity format.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
base_path: Base path for the entity.
|
|
82
|
+
name: Chat name.
|
|
83
|
+
messages: List of chat messages.
|
|
84
|
+
created_at: Creation timestamp.
|
|
85
|
+
modified_at: Modification timestamp.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
EntityMarkdown instance representing the conversation.
|
|
89
|
+
"""
|
|
90
|
+
# Generate permalink
|
|
91
|
+
date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
|
|
92
|
+
clean_title = clean_filename(name)
|
|
93
|
+
permalink = f"{base_path.name}/{date_prefix}-{clean_title}"
|
|
94
|
+
|
|
95
|
+
# Format content
|
|
96
|
+
content = self._format_chat_markdown(
|
|
97
|
+
name=name,
|
|
98
|
+
messages=messages,
|
|
99
|
+
created_at=created_at,
|
|
100
|
+
modified_at=modified_at,
|
|
101
|
+
permalink=permalink,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Create entity
|
|
105
|
+
entity = EntityMarkdown(
|
|
106
|
+
frontmatter=EntityFrontmatter(
|
|
107
|
+
metadata={
|
|
108
|
+
"type": "conversation",
|
|
109
|
+
"title": name,
|
|
110
|
+
"created": created_at,
|
|
111
|
+
"modified": modified_at,
|
|
112
|
+
"permalink": permalink,
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
content=content,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return entity
|
|
119
|
+
|
|
120
|
+
def _format_chat_markdown(
|
|
121
|
+
self,
|
|
122
|
+
name: str,
|
|
123
|
+
messages: List[Dict[str, Any]],
|
|
124
|
+
created_at: str,
|
|
125
|
+
modified_at: str,
|
|
126
|
+
permalink: str,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Format chat as clean markdown.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
name: Chat name.
|
|
132
|
+
messages: List of chat messages.
|
|
133
|
+
created_at: Creation timestamp.
|
|
134
|
+
modified_at: Modification timestamp.
|
|
135
|
+
permalink: Permalink for the entity.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Formatted markdown content.
|
|
139
|
+
"""
|
|
140
|
+
# Start with frontmatter and title
|
|
141
|
+
lines = [
|
|
142
|
+
f"# {name}\n",
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# Add messages
|
|
146
|
+
for msg in messages:
|
|
147
|
+
# Format timestamp
|
|
148
|
+
ts = format_timestamp(msg["created_at"])
|
|
149
|
+
|
|
150
|
+
# Add message header
|
|
151
|
+
lines.append(f"### {msg['sender'].title()} ({ts})")
|
|
152
|
+
|
|
153
|
+
# Handle message content
|
|
154
|
+
content = msg.get("text", "")
|
|
155
|
+
if msg.get("content"):
|
|
156
|
+
# Filter out None values before joining
|
|
157
|
+
content = " ".join(
|
|
158
|
+
str(c.get("text", ""))
|
|
159
|
+
for c in msg["content"]
|
|
160
|
+
if c and c.get("text") is not None
|
|
161
|
+
)
|
|
162
|
+
lines.append(content)
|
|
163
|
+
|
|
164
|
+
# Handle attachments
|
|
165
|
+
attachments = msg.get("attachments", [])
|
|
166
|
+
for attachment in attachments:
|
|
167
|
+
if "file_name" in attachment:
|
|
168
|
+
lines.append(f"\n**Attachment: {attachment['file_name']}**")
|
|
169
|
+
if "extracted_content" in attachment:
|
|
170
|
+
lines.append("```")
|
|
171
|
+
lines.append(attachment["extracted_content"])
|
|
172
|
+
lines.append("```")
|
|
173
|
+
|
|
174
|
+
# Add spacing between messages
|
|
175
|
+
lines.append("")
|
|
176
|
+
|
|
177
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Claude projects import service for Basic Memory."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
|
|
7
|
+
from basic_memory.importers.base import Importer
|
|
8
|
+
from basic_memory.schemas.importer import ProjectImportResult
|
|
9
|
+
from basic_memory.importers.utils import clean_filename
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClaudeProjectsImporter(Importer[ProjectImportResult]):
|
|
15
|
+
"""Service for importing Claude projects."""
|
|
16
|
+
|
|
17
|
+
async def import_data(
|
|
18
|
+
self, source_data, destination_folder: str, **kwargs: Any
|
|
19
|
+
) -> ProjectImportResult:
|
|
20
|
+
"""Import projects from Claude JSON export.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
source_path: Path to the Claude projects.json file.
|
|
24
|
+
destination_folder: Base folder for projects within the project.
|
|
25
|
+
**kwargs: Additional keyword arguments.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
ProjectImportResult containing statistics and status of the import.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
# Ensure the base folder exists
|
|
32
|
+
base_path = self.base_path
|
|
33
|
+
if destination_folder:
|
|
34
|
+
base_path = self.ensure_folder_exists(destination_folder)
|
|
35
|
+
|
|
36
|
+
projects = source_data
|
|
37
|
+
|
|
38
|
+
# Process each project
|
|
39
|
+
docs_imported = 0
|
|
40
|
+
prompts_imported = 0
|
|
41
|
+
|
|
42
|
+
for project in projects:
|
|
43
|
+
project_dir = clean_filename(project["name"])
|
|
44
|
+
|
|
45
|
+
# Create project directories
|
|
46
|
+
docs_dir = base_path / project_dir / "docs"
|
|
47
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
# Import prompt template if it exists
|
|
50
|
+
if prompt_entity := self._format_prompt_markdown(project):
|
|
51
|
+
file_path = base_path / f"{prompt_entity.frontmatter.metadata['permalink']}.md"
|
|
52
|
+
await self.write_entity(prompt_entity, file_path)
|
|
53
|
+
prompts_imported += 1
|
|
54
|
+
|
|
55
|
+
# Import project documents
|
|
56
|
+
for doc in project.get("docs", []):
|
|
57
|
+
entity = self._format_project_markdown(project, doc)
|
|
58
|
+
file_path = base_path / f"{entity.frontmatter.metadata['permalink']}.md"
|
|
59
|
+
await self.write_entity(entity, file_path)
|
|
60
|
+
docs_imported += 1
|
|
61
|
+
|
|
62
|
+
return ProjectImportResult(
|
|
63
|
+
import_count={"documents": docs_imported, "prompts": prompts_imported},
|
|
64
|
+
success=True,
|
|
65
|
+
documents=docs_imported,
|
|
66
|
+
prompts=prompts_imported,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
except Exception as e: # pragma: no cover
|
|
70
|
+
logger.exception("Failed to import Claude projects")
|
|
71
|
+
return self.handle_error("Failed to import Claude projects", e) # pyright: ignore [reportReturnType]
|
|
72
|
+
|
|
73
|
+
def _format_project_markdown(
|
|
74
|
+
self, project: Dict[str, Any], doc: Dict[str, Any]
|
|
75
|
+
) -> EntityMarkdown:
|
|
76
|
+
"""Format a project document as a Basic Memory entity.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
project: Project data.
|
|
80
|
+
doc: Document data.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
EntityMarkdown instance representing the document.
|
|
84
|
+
"""
|
|
85
|
+
# Extract timestamps
|
|
86
|
+
created_at = doc.get("created_at") or project["created_at"]
|
|
87
|
+
modified_at = project["updated_at"]
|
|
88
|
+
|
|
89
|
+
# Generate clean names for organization
|
|
90
|
+
project_dir = clean_filename(project["name"])
|
|
91
|
+
doc_file = clean_filename(doc["filename"])
|
|
92
|
+
|
|
93
|
+
# Create entity
|
|
94
|
+
entity = EntityMarkdown(
|
|
95
|
+
frontmatter=EntityFrontmatter(
|
|
96
|
+
metadata={
|
|
97
|
+
"type": "project_doc",
|
|
98
|
+
"title": doc["filename"],
|
|
99
|
+
"created": created_at,
|
|
100
|
+
"modified": modified_at,
|
|
101
|
+
"permalink": f"{project_dir}/docs/{doc_file}",
|
|
102
|
+
"project_name": project["name"],
|
|
103
|
+
"project_uuid": project["uuid"],
|
|
104
|
+
"doc_uuid": doc["uuid"],
|
|
105
|
+
}
|
|
106
|
+
),
|
|
107
|
+
content=doc["content"],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return entity
|
|
111
|
+
|
|
112
|
+
def _format_prompt_markdown(self, project: Dict[str, Any]) -> Optional[EntityMarkdown]:
|
|
113
|
+
"""Format project prompt template as a Basic Memory entity.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
project: Project data.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
EntityMarkdown instance representing the prompt template, or None if
|
|
120
|
+
no prompt template exists.
|
|
121
|
+
"""
|
|
122
|
+
if not project.get("prompt_template"):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Extract timestamps
|
|
126
|
+
created_at = project["created_at"]
|
|
127
|
+
modified_at = project["updated_at"]
|
|
128
|
+
|
|
129
|
+
# Generate clean project directory name
|
|
130
|
+
project_dir = clean_filename(project["name"])
|
|
131
|
+
|
|
132
|
+
# Create entity
|
|
133
|
+
entity = EntityMarkdown(
|
|
134
|
+
frontmatter=EntityFrontmatter(
|
|
135
|
+
metadata={
|
|
136
|
+
"type": "prompt_template",
|
|
137
|
+
"title": f"Prompt Template: {project['name']}",
|
|
138
|
+
"created": created_at,
|
|
139
|
+
"modified": modified_at,
|
|
140
|
+
"permalink": f"{project_dir}/prompt-template",
|
|
141
|
+
"project_name": project["name"],
|
|
142
|
+
"project_uuid": project["uuid"],
|
|
143
|
+
}
|
|
144
|
+
),
|
|
145
|
+
content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return entity
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Memory JSON import service for Basic Memory."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from basic_memory.config import get_project_config
|
|
7
|
+
from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown, Observation, Relation
|
|
8
|
+
from basic_memory.importers.base import Importer
|
|
9
|
+
from basic_memory.schemas.importer import EntityImportResult
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryJsonImporter(Importer[EntityImportResult]):
|
|
15
|
+
"""Service for importing memory.json format data."""
|
|
16
|
+
|
|
17
|
+
async def import_data(
|
|
18
|
+
self, source_data, destination_folder: str = "", **kwargs: Any
|
|
19
|
+
) -> EntityImportResult:
|
|
20
|
+
"""Import entities and relations from a memory.json file.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
source_data: Path to the memory.json file.
|
|
24
|
+
destination_folder: Optional destination folder within the project.
|
|
25
|
+
**kwargs: Additional keyword arguments.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
EntityImportResult containing statistics and status of the import.
|
|
29
|
+
"""
|
|
30
|
+
config = get_project_config()
|
|
31
|
+
try:
|
|
32
|
+
# First pass - collect all relations by source entity
|
|
33
|
+
entity_relations: Dict[str, List[Relation]] = {}
|
|
34
|
+
entities: Dict[str, Dict[str, Any]] = {}
|
|
35
|
+
skipped_entities: int = 0
|
|
36
|
+
|
|
37
|
+
# Ensure the base path exists
|
|
38
|
+
base_path = config.home # pragma: no cover
|
|
39
|
+
if destination_folder: # pragma: no cover
|
|
40
|
+
base_path = self.ensure_folder_exists(destination_folder)
|
|
41
|
+
|
|
42
|
+
# First pass - collect entities and relations
|
|
43
|
+
for line in source_data:
|
|
44
|
+
data = line
|
|
45
|
+
if data["type"] == "entity":
|
|
46
|
+
# Handle different possible name keys
|
|
47
|
+
entity_name = data.get("name") or data.get("entityName") or data.get("id")
|
|
48
|
+
if not entity_name:
|
|
49
|
+
logger.warning(f"Entity missing name field: {data}")
|
|
50
|
+
skipped_entities += 1
|
|
51
|
+
continue
|
|
52
|
+
entities[entity_name] = data
|
|
53
|
+
elif data["type"] == "relation":
|
|
54
|
+
# Store relation with its source entity
|
|
55
|
+
source = data.get("from") or data.get("from_id")
|
|
56
|
+
if source not in entity_relations:
|
|
57
|
+
entity_relations[source] = []
|
|
58
|
+
entity_relations[source].append(
|
|
59
|
+
Relation(
|
|
60
|
+
type=data.get("relationType") or data.get("relation_type"),
|
|
61
|
+
target=data.get("to") or data.get("to_id"),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Second pass - create and write entities
|
|
66
|
+
entities_created = 0
|
|
67
|
+
for name, entity_data in entities.items():
|
|
68
|
+
# Get entity type with fallback
|
|
69
|
+
entity_type = entity_data.get("entityType") or entity_data.get("type") or "entity"
|
|
70
|
+
|
|
71
|
+
# Ensure entity type directory exists
|
|
72
|
+
entity_type_dir = base_path / entity_type
|
|
73
|
+
entity_type_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
# Get observations with fallback to empty list
|
|
76
|
+
observations = entity_data.get("observations", [])
|
|
77
|
+
|
|
78
|
+
entity = EntityMarkdown(
|
|
79
|
+
frontmatter=EntityFrontmatter(
|
|
80
|
+
metadata={
|
|
81
|
+
"type": entity_type,
|
|
82
|
+
"title": name,
|
|
83
|
+
"permalink": f"{entity_type}/{name}",
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
content=f"# {name}\n",
|
|
87
|
+
observations=[Observation(content=obs) for obs in observations],
|
|
88
|
+
relations=entity_relations.get(name, []),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Write entity file
|
|
92
|
+
file_path = base_path / f"{entity_type}/{name}.md"
|
|
93
|
+
await self.write_entity(entity, file_path)
|
|
94
|
+
entities_created += 1
|
|
95
|
+
|
|
96
|
+
relations_count = sum(len(rels) for rels in entity_relations.values())
|
|
97
|
+
|
|
98
|
+
return EntityImportResult(
|
|
99
|
+
import_count={"entities": entities_created, "relations": relations_count},
|
|
100
|
+
success=True,
|
|
101
|
+
entities=entities_created,
|
|
102
|
+
relations=relations_count,
|
|
103
|
+
skipped_entities=skipped_entities,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
except Exception as e: # pragma: no cover
|
|
107
|
+
logger.exception("Failed to import memory.json")
|
|
108
|
+
return self.handle_error("Failed to import memory.json", e) # pyright: ignore [reportReturnType]
|