Skip to content

Commit 887da3f

Browse files
jope-bmclaude
andcommitted
feat: Add v2 import endpoints for ChatGPT, Claude, and memory JSON
Implements v2 versions of all import endpoints: - POST /v2/projects/{project_id}/import/chatgpt - POST /v2/projects/{project_id}/import/claude/conversations - POST /v2/projects/{project_id}/import/claude/projects - POST /v2/projects/{project_id}/import/memory-json These endpoints use v2 dependencies (ProjectConfigV2Dep, MarkdownProcessorV2Dep) for consistent ID-based operations. Changes: - Added v2 importer dependencies in deps.py - Created v2 importer router with all four import endpoints - Comprehensive test suite with 14 test cases covering: - All import formats (ChatGPT, Claude conversations/projects, memory JSON) - Error handling (invalid files, malformed JSON, empty files) - V2 path validation (project ID vs name) - Invalid project ID handling All 14 SQLite tests pass successfully. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e1174f3 commit 887da3f

6 files changed

Lines changed: 752 additions & 0 deletions

File tree

src/basic_memory/api/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
resource_router as v2_resource,
2929
directory_router as v2_directory,
3030
prompt_router as v2_prompt,
31+
importer_router as v2_importer,
3132
)
3233
from basic_memory.config import ConfigManager
3334
from basic_memory.services.initialization import initialize_file_sync, initialize_app
@@ -92,6 +93,7 @@ async def lifespan(app: FastAPI): # pragma: no cover
9293
app.include_router(v2_resource, prefix="/v2/projects/{project_id}")
9394
app.include_router(v2_directory, prefix="/v2/projects/{project_id}")
9495
app.include_router(v2_prompt, prefix="/v2/projects/{project_id}")
96+
app.include_router(v2_importer, prefix="/v2/projects/{project_id}")
9597
app.include_router(v2_project, prefix="/v2")
9698

9799
# Project resource router works across projects

src/basic_memory/api/v2/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
search_router,
2121
directory_router,
2222
prompt_router,
23+
importer_router,
2324
)
2425

2526
__all__ = [
@@ -30,4 +31,5 @@
3031
"search_router",
3132
"directory_router",
3233
"prompt_router",
34+
"importer_router",
3335
]

src/basic_memory/api/v2/routers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from basic_memory.api.v2.routers.resource_router import router as resource_router
88
from basic_memory.api.v2.routers.directory_router import router as directory_router
99
from basic_memory.api.v2.routers.prompt_router import router as prompt_router
10+
from basic_memory.api.v2.routers.importer_router import router as importer_router
1011

1112
__all__ = [
1213
"knowledge_router",
@@ -16,4 +17,5 @@
1617
"resource_router",
1718
"directory_router",
1819
"prompt_router",
20+
"importer_router",
1921
]
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""V2 Import Router - ID-based data import operations.
2+
3+
This router uses v2 dependencies for consistent project ID handling.
4+
Import endpoints use project_id in the path for consistency with other v2 endpoints.
5+
"""
6+
7+
import json
8+
import logging
9+
10+
from fastapi import APIRouter, Form, HTTPException, UploadFile, status
11+
12+
from basic_memory.deps import (
13+
ChatGPTImporterV2Dep,
14+
ClaudeConversationsImporterV2Dep,
15+
ClaudeProjectsImporterV2Dep,
16+
MemoryJsonImporterV2Dep,
17+
ProjectIdPathDep,
18+
)
19+
from basic_memory.importers import Importer
20+
from basic_memory.schemas.importer import (
21+
ChatImportResult,
22+
EntityImportResult,
23+
ProjectImportResult,
24+
)
25+
26+
logger = logging.getLogger(__name__)
27+
28+
router = APIRouter(prefix="/import", tags=["import-v2"])
29+
30+
31+
@router.post("/chatgpt", response_model=ChatImportResult)
32+
async def import_chatgpt(
33+
project_id: ProjectIdPathDep,
34+
importer: ChatGPTImporterV2Dep,
35+
file: UploadFile,
36+
folder: str = Form("conversations"),
37+
) -> ChatImportResult:
38+
"""Import conversations from ChatGPT JSON export.
39+
40+
Args:
41+
project_id: Validated numeric project ID from URL path
42+
file: The ChatGPT conversations.json file.
43+
folder: The folder to place the files in.
44+
importer: ChatGPT importer instance.
45+
46+
Returns:
47+
ChatImportResult with import statistics.
48+
49+
Raises:
50+
HTTPException: If import fails.
51+
"""
52+
logger.info(f"V2 Importing ChatGPT conversations for project {project_id}")
53+
return await import_file(importer, file, folder)
54+
55+
56+
@router.post("/claude/conversations", response_model=ChatImportResult)
57+
async def import_claude_conversations(
58+
project_id: ProjectIdPathDep,
59+
importer: ClaudeConversationsImporterV2Dep,
60+
file: UploadFile,
61+
folder: str = Form("conversations"),
62+
) -> ChatImportResult:
63+
"""Import conversations from Claude conversations.json export.
64+
65+
Args:
66+
project_id: Validated numeric project ID from URL path
67+
file: The Claude conversations.json file.
68+
folder: The folder to place the files in.
69+
importer: Claude conversations importer instance.
70+
71+
Returns:
72+
ChatImportResult with import statistics.
73+
74+
Raises:
75+
HTTPException: If import fails.
76+
"""
77+
logger.info(f"V2 Importing Claude conversations for project {project_id}")
78+
return await import_file(importer, file, folder)
79+
80+
81+
@router.post("/claude/projects", response_model=ProjectImportResult)
82+
async def import_claude_projects(
83+
project_id: ProjectIdPathDep,
84+
importer: ClaudeProjectsImporterV2Dep,
85+
file: UploadFile,
86+
folder: str = Form("projects"),
87+
) -> ProjectImportResult:
88+
"""Import projects from Claude projects.json export.
89+
90+
Args:
91+
project_id: Validated numeric project ID from URL path
92+
file: The Claude projects.json file.
93+
folder: The base folder to place the files in.
94+
importer: Claude projects importer instance.
95+
96+
Returns:
97+
ProjectImportResult with import statistics.
98+
99+
Raises:
100+
HTTPException: If import fails.
101+
"""
102+
logger.info(f"V2 Importing Claude projects for project {project_id}")
103+
return await import_file(importer, file, folder)
104+
105+
106+
@router.post("/memory-json", response_model=EntityImportResult)
107+
async def import_memory_json(
108+
project_id: ProjectIdPathDep,
109+
importer: MemoryJsonImporterV2Dep,
110+
file: UploadFile,
111+
folder: str = Form("conversations"),
112+
) -> EntityImportResult:
113+
"""Import entities and relations from a memory.json file.
114+
115+
Args:
116+
project_id: Validated numeric project ID from URL path
117+
file: The memory.json file.
118+
folder: Optional destination folder within the project.
119+
importer: Memory JSON importer instance.
120+
121+
Returns:
122+
EntityImportResult with import statistics.
123+
124+
Raises:
125+
HTTPException: If import fails.
126+
"""
127+
logger.info(f"V2 Importing memory.json for project {project_id}")
128+
try:
129+
file_data = []
130+
file_bytes = await file.read()
131+
file_str = file_bytes.decode("utf-8")
132+
for line in file_str.splitlines():
133+
json_data = json.loads(line)
134+
file_data.append(json_data)
135+
136+
result = await importer.import_data(file_data, folder)
137+
if not result.success: # pragma: no cover
138+
raise HTTPException(
139+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
140+
detail=result.error_message or "Import failed",
141+
)
142+
except Exception as e:
143+
logger.exception("V2 Import failed")
144+
raise HTTPException(
145+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
146+
detail=f"Import failed: {str(e)}",
147+
)
148+
return result
149+
150+
151+
async def import_file(importer: Importer, file: UploadFile, destination_folder: str):
152+
"""Helper function to import a file using an importer instance.
153+
154+
Args:
155+
importer: The importer instance to use
156+
file: The file to import
157+
destination_folder: Destination folder for imported content
158+
159+
Returns:
160+
Import result from the importer
161+
162+
Raises:
163+
HTTPException: If import fails
164+
"""
165+
try:
166+
# Process file
167+
json_data = json.load(file.file)
168+
result = await importer.import_data(json_data, destination_folder)
169+
if not result.success: # pragma: no cover
170+
raise HTTPException(
171+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
172+
detail=result.error_message or "Import failed",
173+
)
174+
175+
return result
176+
177+
except Exception as e:
178+
logger.exception("V2 Import failed")
179+
raise HTTPException(
180+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
181+
detail=f"Import failed: {str(e)}",
182+
)

src/basic_memory/deps.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,50 @@ async def get_memory_json_importer(
644644

645645

646646
MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)]
647+
648+
649+
# V2 Import dependencies
650+
651+
652+
async def get_chatgpt_importer_v2(
653+
project_config: ProjectConfigV2Dep, markdown_processor: MarkdownProcessorV2Dep
654+
) -> ChatGPTImporter:
655+
"""Create ChatGPTImporter with v2 dependencies."""
656+
return ChatGPTImporter(project_config.home, markdown_processor)
657+
658+
659+
ChatGPTImporterV2Dep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer_v2)]
660+
661+
662+
async def get_claude_conversations_importer_v2(
663+
project_config: ProjectConfigV2Dep, markdown_processor: MarkdownProcessorV2Dep
664+
) -> ClaudeConversationsImporter:
665+
"""Create ClaudeConversationsImporter with v2 dependencies."""
666+
return ClaudeConversationsImporter(project_config.home, markdown_processor)
667+
668+
669+
ClaudeConversationsImporterV2Dep = Annotated[
670+
ClaudeConversationsImporter, Depends(get_claude_conversations_importer_v2)
671+
]
672+
673+
674+
async def get_claude_projects_importer_v2(
675+
project_config: ProjectConfigV2Dep, markdown_processor: MarkdownProcessorV2Dep
676+
) -> ClaudeProjectsImporter:
677+
"""Create ClaudeProjectsImporter with v2 dependencies."""
678+
return ClaudeProjectsImporter(project_config.home, markdown_processor)
679+
680+
681+
ClaudeProjectsImporterV2Dep = Annotated[
682+
ClaudeProjectsImporter, Depends(get_claude_projects_importer_v2)
683+
]
684+
685+
686+
async def get_memory_json_importer_v2(
687+
project_config: ProjectConfigV2Dep, markdown_processor: MarkdownProcessorV2Dep
688+
) -> MemoryJsonImporter:
689+
"""Create MemoryJsonImporter with v2 dependencies."""
690+
return MemoryJsonImporter(project_config.home, markdown_processor)
691+
692+
693+
MemoryJsonImporterV2Dep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer_v2)]

0 commit comments

Comments
 (0)