basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 +7 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
- 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 +127 -38
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +4 -59
- basic_memory/api/routers/project_router.py +230 -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 +99 -67
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +145 -88
- 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/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +19 -3
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +82 -8
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +20 -0
- basic_memory/mcp/tools/build_context.py +11 -1
- basic_memory/mcp/tools/canvas.py +15 -2
- basic_memory/mcp/tools/delete_note.py +12 -4
- basic_memory/mcp/tools/edit_note.py +297 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +87 -0
- basic_memory/mcp/tools/project_management.py +300 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +17 -5
- basic_memory/mcp/tools/recent_activity.py +11 -2
- basic_memory/mcp/tools/search.py +10 -1
- basic_memory/mcp/tools/utils.py +137 -12
- basic_memory/mcp/tools/write_note.py +11 -15
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +80 -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 +87 -27
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +26 -12
- 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 +385 -5
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +144 -67
- basic_memory/services/link_resolver.py +16 -8
- basic_memory/services/project_service.py +548 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +10 -9
- 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-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b1.dist-info/RECORD +132 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.3.dist-info/RECORD +0 -100
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
try:
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
__version__ = version("basic-memory")
|
|
7
|
+
except Exception: # pragma: no cover
|
|
8
|
+
# Fallback if package not installed (e.g., during development)
|
|
9
|
+
__version__ = "0.0.0" # pragma: no cover
|
basic_memory/alembic/env.py
CHANGED
|
@@ -13,7 +13,7 @@ from basic_memory.models import Base
|
|
|
13
13
|
# set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
|
|
14
14
|
os.environ["BASIC_MEMORY_ENV"] = "test"
|
|
15
15
|
|
|
16
|
-
from basic_memory.config import
|
|
16
|
+
from basic_memory.config import app_config
|
|
17
17
|
|
|
18
18
|
# this is the Alembic Config object, which provides
|
|
19
19
|
# access to the values within the .ini file in use.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""add projects table
|
|
2
|
+
|
|
3
|
+
Revision ID: 5fe1ab1ccebe
|
|
4
|
+
Revises: cc7172b46608
|
|
5
|
+
Create Date: 2025-05-14 09:05:18.214357
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "5fe1ab1ccebe"
|
|
17
|
+
down_revision: Union[str, None] = "cc7172b46608"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
24
|
+
op.create_table(
|
|
25
|
+
"project",
|
|
26
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
27
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
28
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
29
|
+
sa.Column("permalink", sa.String(), nullable=False),
|
|
30
|
+
sa.Column("path", sa.String(), nullable=False),
|
|
31
|
+
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
32
|
+
sa.Column("is_default", sa.Boolean(), nullable=True),
|
|
33
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
34
|
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
|
35
|
+
sa.PrimaryKeyConstraint("id"),
|
|
36
|
+
sa.UniqueConstraint("is_default"),
|
|
37
|
+
sa.UniqueConstraint("name"),
|
|
38
|
+
sa.UniqueConstraint("permalink"),
|
|
39
|
+
if_not_exists=True,
|
|
40
|
+
)
|
|
41
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
42
|
+
batch_op.create_index(
|
|
43
|
+
"ix_project_created_at", ["created_at"], unique=False, if_not_exists=True
|
|
44
|
+
)
|
|
45
|
+
batch_op.create_index("ix_project_name", ["name"], unique=True, if_not_exists=True)
|
|
46
|
+
batch_op.create_index("ix_project_path", ["path"], unique=False, if_not_exists=True)
|
|
47
|
+
batch_op.create_index(
|
|
48
|
+
"ix_project_permalink", ["permalink"], unique=True, if_not_exists=True
|
|
49
|
+
)
|
|
50
|
+
batch_op.create_index(
|
|
51
|
+
"ix_project_updated_at", ["updated_at"], unique=False, if_not_exists=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
with op.batch_alter_table("entity", schema=None) as batch_op:
|
|
55
|
+
batch_op.add_column(sa.Column("project_id", sa.Integer(), nullable=False))
|
|
56
|
+
batch_op.drop_index(
|
|
57
|
+
"uix_entity_permalink",
|
|
58
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
59
|
+
)
|
|
60
|
+
batch_op.drop_index("ix_entity_file_path")
|
|
61
|
+
batch_op.create_index(batch_op.f("ix_entity_file_path"), ["file_path"], unique=False)
|
|
62
|
+
batch_op.create_index("ix_entity_project_id", ["project_id"], unique=False)
|
|
63
|
+
batch_op.create_index(
|
|
64
|
+
"uix_entity_file_path_project", ["file_path", "project_id"], unique=True
|
|
65
|
+
)
|
|
66
|
+
batch_op.create_index(
|
|
67
|
+
"uix_entity_permalink_project",
|
|
68
|
+
["permalink", "project_id"],
|
|
69
|
+
unique=True,
|
|
70
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
71
|
+
)
|
|
72
|
+
batch_op.create_foreign_key("fk_entity_project_id", "project", ["project_id"], ["id"])
|
|
73
|
+
|
|
74
|
+
# drop the search index table. it will be recreated
|
|
75
|
+
op.drop_table("search_index")
|
|
76
|
+
|
|
77
|
+
# ### end Alembic commands ###
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def downgrade() -> None:
|
|
81
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
82
|
+
with op.batch_alter_table("entity", schema=None) as batch_op:
|
|
83
|
+
batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
|
|
84
|
+
batch_op.drop_index(
|
|
85
|
+
"uix_entity_permalink_project",
|
|
86
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
87
|
+
)
|
|
88
|
+
batch_op.drop_index("uix_entity_file_path_project")
|
|
89
|
+
batch_op.drop_index("ix_entity_project_id")
|
|
90
|
+
batch_op.drop_index(batch_op.f("ix_entity_file_path"))
|
|
91
|
+
batch_op.create_index("ix_entity_file_path", ["file_path"], unique=1)
|
|
92
|
+
batch_op.create_index(
|
|
93
|
+
"uix_entity_permalink",
|
|
94
|
+
["permalink"],
|
|
95
|
+
unique=1,
|
|
96
|
+
sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
97
|
+
)
|
|
98
|
+
batch_op.drop_column("project_id")
|
|
99
|
+
|
|
100
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
101
|
+
batch_op.drop_index("ix_project_updated_at")
|
|
102
|
+
batch_op.drop_index("ix_project_permalink")
|
|
103
|
+
batch_op.drop_index("ix_project_path")
|
|
104
|
+
batch_op.drop_index("ix_project_name")
|
|
105
|
+
batch_op.drop_index("ix_project_created_at")
|
|
106
|
+
|
|
107
|
+
op.drop_table("project")
|
|
108
|
+
# ### end Alembic commands ###
|
|
@@ -56,11 +56,6 @@ def upgrade() -> None:
|
|
|
56
56
|
);
|
|
57
57
|
""")
|
|
58
58
|
|
|
59
|
-
# Print instruction to manually reindex after migration
|
|
60
|
-
print("\n------------------------------------------------------------------")
|
|
61
|
-
print("IMPORTANT: After migration completes, manually run the reindex command:")
|
|
62
|
-
print("basic-memory sync")
|
|
63
|
-
print("------------------------------------------------------------------\n")
|
|
64
59
|
|
|
65
60
|
|
|
66
61
|
def downgrade() -> None:
|
basic_memory/api/app.py
CHANGED
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
"""FastAPI application for basic-memory knowledge graph API."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
5
|
|
|
5
6
|
from fastapi import FastAPI, HTTPException
|
|
6
7
|
from fastapi.exception_handlers import http_exception_handler
|
|
7
8
|
from loguru import logger
|
|
8
9
|
|
|
10
|
+
from basic_memory import __version__ as version
|
|
9
11
|
from basic_memory import db
|
|
10
|
-
from basic_memory.api.routers import
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
from basic_memory.api.routers import (
|
|
13
|
+
directory_router,
|
|
14
|
+
importer_router,
|
|
15
|
+
knowledge,
|
|
16
|
+
management,
|
|
17
|
+
memory,
|
|
18
|
+
project,
|
|
19
|
+
resource,
|
|
20
|
+
search,
|
|
21
|
+
prompt_router,
|
|
22
|
+
)
|
|
23
|
+
from basic_memory.config import app_config
|
|
24
|
+
from basic_memory.services.initialization import initialize_app, initialize_file_sync
|
|
13
25
|
|
|
14
26
|
|
|
15
27
|
@asynccontextmanager
|
|
16
28
|
async def lifespan(app: FastAPI): # pragma: no cover
|
|
17
29
|
"""Lifecycle manager for the FastAPI app."""
|
|
18
|
-
# Initialize
|
|
19
|
-
|
|
30
|
+
# Initialize app and database
|
|
31
|
+
logger.info("Starting Basic Memory API")
|
|
32
|
+
await initialize_app(app_config)
|
|
33
|
+
|
|
34
|
+
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
|
35
|
+
if app_config.sync_changes:
|
|
36
|
+
# start file sync task in background
|
|
37
|
+
app.state.sync_task = asyncio.create_task(initialize_file_sync(app_config))
|
|
38
|
+
else:
|
|
39
|
+
logger.info("Sync changes disabled. Skipping file sync service.")
|
|
20
40
|
|
|
21
41
|
# proceed with startup
|
|
22
42
|
yield
|
|
23
43
|
|
|
24
44
|
logger.info("Shutting down Basic Memory API")
|
|
25
|
-
if
|
|
26
|
-
|
|
45
|
+
if app.state.sync_task:
|
|
46
|
+
logger.info("Stopping sync...")
|
|
47
|
+
app.state.sync_task.cancel() # pyright: ignore
|
|
27
48
|
|
|
28
49
|
await db.shutdown_db()
|
|
29
50
|
|
|
@@ -32,17 +53,26 @@ async def lifespan(app: FastAPI): # pragma: no cover
|
|
|
32
53
|
app = FastAPI(
|
|
33
54
|
title="Basic Memory API",
|
|
34
55
|
description="Knowledge graph API for basic-memory",
|
|
35
|
-
version=
|
|
56
|
+
version=version,
|
|
36
57
|
lifespan=lifespan,
|
|
37
58
|
)
|
|
38
59
|
|
|
39
60
|
|
|
40
61
|
# Include routers
|
|
41
|
-
app.include_router(knowledge.router)
|
|
42
|
-
app.include_router(
|
|
43
|
-
app.include_router(
|
|
44
|
-
app.include_router(
|
|
45
|
-
app.include_router(
|
|
62
|
+
app.include_router(knowledge.router, prefix="/{project}")
|
|
63
|
+
app.include_router(memory.router, prefix="/{project}")
|
|
64
|
+
app.include_router(resource.router, prefix="/{project}")
|
|
65
|
+
app.include_router(search.router, prefix="/{project}")
|
|
66
|
+
app.include_router(project.project_router, prefix="/{project}")
|
|
67
|
+
app.include_router(directory_router.router, prefix="/{project}")
|
|
68
|
+
app.include_router(prompt_router.router, prefix="/{project}")
|
|
69
|
+
app.include_router(importer_router.router, prefix="/{project}")
|
|
70
|
+
|
|
71
|
+
# Project resource router works accross projects
|
|
72
|
+
app.include_router(project.project_resource_router)
|
|
73
|
+
app.include_router(management.router)
|
|
74
|
+
|
|
75
|
+
# Auth routes are handled by FastMCP automatically when auth is enabled
|
|
46
76
|
|
|
47
77
|
|
|
48
78
|
@app.exception_handler(Exception)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""API routers."""
|
|
2
2
|
|
|
3
3
|
from . import knowledge_router as knowledge
|
|
4
|
+
from . import management_router as management
|
|
4
5
|
from . import memory_router as memory
|
|
6
|
+
from . import project_router as project
|
|
5
7
|
from . import resource_router as resource
|
|
6
8
|
from . import search_router as search
|
|
7
|
-
from . import
|
|
9
|
+
from . import prompt_router as prompt
|
|
8
10
|
|
|
9
|
-
__all__ = ["knowledge", "memory", "resource", "search", "
|
|
11
|
+
__all__ = ["knowledge", "management", "memory", "project", "resource", "search", "prompt"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Router for directory tree operations."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Query
|
|
6
|
+
|
|
7
|
+
from basic_memory.deps import DirectoryServiceDep, ProjectIdDep
|
|
8
|
+
from basic_memory.schemas.directory import DirectoryNode
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/directory", tags=["directory"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/tree", response_model=DirectoryNode)
|
|
14
|
+
async def get_directory_tree(
|
|
15
|
+
directory_service: DirectoryServiceDep,
|
|
16
|
+
project_id: ProjectIdDep,
|
|
17
|
+
):
|
|
18
|
+
"""Get hierarchical directory structure from the knowledge base.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
directory_service: Service for directory operations
|
|
22
|
+
project_id: ID of the current project
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
DirectoryNode representing the root of the hierarchical tree structure
|
|
26
|
+
"""
|
|
27
|
+
# Get a hierarchical directory tree for the specific project
|
|
28
|
+
tree = await directory_service.get_directory_tree()
|
|
29
|
+
|
|
30
|
+
# Return the hierarchical tree
|
|
31
|
+
return tree
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get("/list", response_model=List[DirectoryNode])
|
|
35
|
+
async def list_directory(
|
|
36
|
+
directory_service: DirectoryServiceDep,
|
|
37
|
+
project_id: ProjectIdDep,
|
|
38
|
+
dir_name: str = Query("/", description="Directory path to list"),
|
|
39
|
+
depth: int = Query(1, ge=1, le=10, description="Recursion depth (1-10)"),
|
|
40
|
+
file_name_glob: Optional[str] = Query(
|
|
41
|
+
None, description="Glob pattern for filtering file names"
|
|
42
|
+
),
|
|
43
|
+
):
|
|
44
|
+
"""List directory contents with filtering and depth control.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
directory_service: Service for directory operations
|
|
48
|
+
project_id: ID of the current project
|
|
49
|
+
dir_name: Directory path to list (default: root "/")
|
|
50
|
+
depth: Recursion depth (1-10, default: 1 for immediate children only)
|
|
51
|
+
file_name_glob: Optional glob pattern for filtering file names (e.g., "*.md", "*meeting*")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of DirectoryNode objects matching the criteria
|
|
55
|
+
"""
|
|
56
|
+
# Get directory listing with filtering
|
|
57
|
+
nodes = await directory_service.list_directory(
|
|
58
|
+
dir_name=dir_name,
|
|
59
|
+
depth=depth,
|
|
60
|
+
file_name_glob=file_name_glob,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return nodes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Import router for Basic Memory API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Form, HTTPException, UploadFile, status
|
|
7
|
+
|
|
8
|
+
from basic_memory.deps import (
|
|
9
|
+
ChatGPTImporterDep,
|
|
10
|
+
ClaudeConversationsImporterDep,
|
|
11
|
+
ClaudeProjectsImporterDep,
|
|
12
|
+
MemoryJsonImporterDep,
|
|
13
|
+
)
|
|
14
|
+
from basic_memory.importers import Importer
|
|
15
|
+
from basic_memory.schemas.importer import (
|
|
16
|
+
ChatImportResult,
|
|
17
|
+
EntityImportResult,
|
|
18
|
+
ProjectImportResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/import", tags=["import"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.post("/chatgpt", response_model=ChatImportResult)
|
|
27
|
+
async def import_chatgpt(
|
|
28
|
+
importer: ChatGPTImporterDep,
|
|
29
|
+
file: UploadFile,
|
|
30
|
+
folder: str = Form("conversations"),
|
|
31
|
+
) -> ChatImportResult:
|
|
32
|
+
"""Import conversations from ChatGPT JSON export.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
file: The ChatGPT conversations.json file.
|
|
36
|
+
folder: The folder to place the files in.
|
|
37
|
+
markdown_processor: MarkdownProcessor instance.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ChatImportResult with import statistics.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
HTTPException: If import fails.
|
|
44
|
+
"""
|
|
45
|
+
return await import_file(importer, file, folder)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.post("/claude/conversations", response_model=ChatImportResult)
|
|
49
|
+
async def import_claude_conversations(
|
|
50
|
+
importer: ClaudeConversationsImporterDep,
|
|
51
|
+
file: UploadFile,
|
|
52
|
+
folder: str = Form("conversations"),
|
|
53
|
+
) -> ChatImportResult:
|
|
54
|
+
"""Import conversations from Claude conversations.json export.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
file: The Claude conversations.json file.
|
|
58
|
+
folder: The folder to place the files in.
|
|
59
|
+
markdown_processor: MarkdownProcessor instance.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
ChatImportResult with import statistics.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
HTTPException: If import fails.
|
|
66
|
+
"""
|
|
67
|
+
return await import_file(importer, file, folder)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.post("/claude/projects", response_model=ProjectImportResult)
|
|
71
|
+
async def import_claude_projects(
|
|
72
|
+
importer: ClaudeProjectsImporterDep,
|
|
73
|
+
file: UploadFile,
|
|
74
|
+
folder: str = Form("projects"),
|
|
75
|
+
) -> ProjectImportResult:
|
|
76
|
+
"""Import projects from Claude projects.json export.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
file: The Claude projects.json file.
|
|
80
|
+
base_folder: The base folder to place the files in.
|
|
81
|
+
markdown_processor: MarkdownProcessor instance.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
ProjectImportResult with import statistics.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
HTTPException: If import fails.
|
|
88
|
+
"""
|
|
89
|
+
return await import_file(importer, file, folder)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@router.post("/memory-json", response_model=EntityImportResult)
|
|
93
|
+
async def import_memory_json(
|
|
94
|
+
importer: MemoryJsonImporterDep,
|
|
95
|
+
file: UploadFile,
|
|
96
|
+
folder: str = Form("conversations"),
|
|
97
|
+
) -> EntityImportResult:
|
|
98
|
+
"""Import entities and relations from a memory.json file.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
file: The memory.json file.
|
|
102
|
+
destination_folder: Optional destination folder within the project.
|
|
103
|
+
markdown_processor: MarkdownProcessor instance.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
EntityImportResult with import statistics.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
HTTPException: If import fails.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
file_data = []
|
|
113
|
+
file_bytes = await file.read()
|
|
114
|
+
file_str = file_bytes.decode("utf-8")
|
|
115
|
+
for line in file_str.splitlines():
|
|
116
|
+
json_data = json.loads(line)
|
|
117
|
+
file_data.append(json_data)
|
|
118
|
+
|
|
119
|
+
result = await importer.import_data(file_data, folder)
|
|
120
|
+
if not result.success: # pragma: no cover
|
|
121
|
+
raise HTTPException(
|
|
122
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
123
|
+
detail=result.error_message or "Import failed",
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.exception("Import failed")
|
|
127
|
+
raise HTTPException(
|
|
128
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
129
|
+
detail=f"Import failed: {str(e)}",
|
|
130
|
+
)
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def import_file(importer: Importer, file: UploadFile, destination_folder: str):
|
|
135
|
+
try:
|
|
136
|
+
# Process file
|
|
137
|
+
json_data = json.load(file.file)
|
|
138
|
+
result = await importer.import_data(json_data, destination_folder)
|
|
139
|
+
if not result.success: # pragma: no cover
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
142
|
+
detail=result.error_message or "Import failed",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.exception("Import failed")
|
|
149
|
+
raise HTTPException(
|
|
150
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
151
|
+
detail=f"Import failed: {str(e)}",
|
|
152
|
+
)
|