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,100 @@
|
|
|
1
|
+
"""Update search index schema
|
|
2
|
+
|
|
3
|
+
Revision ID: cc7172b46608
|
|
4
|
+
Revises: 502b60eaa905
|
|
5
|
+
Create Date: 2025-02-28 18:48:23.244941
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = "cc7172b46608"
|
|
16
|
+
down_revision: Union[str, None] = "502b60eaa905"
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
"""Upgrade database schema to use new search index with content_stems and content_snippet."""
|
|
23
|
+
|
|
24
|
+
# First, drop the existing search_index table
|
|
25
|
+
op.execute("DROP TABLE IF EXISTS search_index")
|
|
26
|
+
|
|
27
|
+
# Create new search_index with updated schema
|
|
28
|
+
op.execute("""
|
|
29
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
|
30
|
+
-- Core entity fields
|
|
31
|
+
id UNINDEXED, -- Row ID
|
|
32
|
+
title, -- Title for searching
|
|
33
|
+
content_stems, -- Main searchable content split into stems
|
|
34
|
+
content_snippet, -- File content snippet for display
|
|
35
|
+
permalink, -- Stable identifier (now indexed for path search)
|
|
36
|
+
file_path UNINDEXED, -- Physical location
|
|
37
|
+
type UNINDEXED, -- entity/relation/observation
|
|
38
|
+
|
|
39
|
+
-- Relation fields
|
|
40
|
+
from_id UNINDEXED, -- Source entity
|
|
41
|
+
to_id UNINDEXED, -- Target entity
|
|
42
|
+
relation_type UNINDEXED, -- Type of relation
|
|
43
|
+
|
|
44
|
+
-- Observation fields
|
|
45
|
+
entity_id UNINDEXED, -- Parent entity
|
|
46
|
+
category UNINDEXED, -- Observation category
|
|
47
|
+
|
|
48
|
+
-- Common fields
|
|
49
|
+
metadata UNINDEXED, -- JSON metadata
|
|
50
|
+
created_at UNINDEXED, -- Creation timestamp
|
|
51
|
+
updated_at UNINDEXED, -- Last update
|
|
52
|
+
|
|
53
|
+
-- Configuration
|
|
54
|
+
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
|
|
55
|
+
prefix='1,2,3,4' -- Support longer prefixes for paths
|
|
56
|
+
);
|
|
57
|
+
""")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def downgrade() -> None:
|
|
61
|
+
"""Downgrade database schema to use old search index."""
|
|
62
|
+
# Drop the updated search_index table
|
|
63
|
+
op.execute("DROP TABLE IF EXISTS search_index")
|
|
64
|
+
|
|
65
|
+
# Recreate the original search_index schema
|
|
66
|
+
op.execute("""
|
|
67
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
|
68
|
+
-- Core entity fields
|
|
69
|
+
id UNINDEXED, -- Row ID
|
|
70
|
+
title, -- Title for searching
|
|
71
|
+
content, -- Main searchable content
|
|
72
|
+
permalink, -- Stable identifier (now indexed for path search)
|
|
73
|
+
file_path UNINDEXED, -- Physical location
|
|
74
|
+
type UNINDEXED, -- entity/relation/observation
|
|
75
|
+
|
|
76
|
+
-- Relation fields
|
|
77
|
+
from_id UNINDEXED, -- Source entity
|
|
78
|
+
to_id UNINDEXED, -- Target entity
|
|
79
|
+
relation_type UNINDEXED, -- Type of relation
|
|
80
|
+
|
|
81
|
+
-- Observation fields
|
|
82
|
+
entity_id UNINDEXED, -- Parent entity
|
|
83
|
+
category UNINDEXED, -- Observation category
|
|
84
|
+
|
|
85
|
+
-- Common fields
|
|
86
|
+
metadata UNINDEXED, -- JSON metadata
|
|
87
|
+
created_at UNINDEXED, -- Creation timestamp
|
|
88
|
+
updated_at UNINDEXED, -- Last update
|
|
89
|
+
|
|
90
|
+
-- Configuration
|
|
91
|
+
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
|
|
92
|
+
prefix='1,2,3,4' -- Support longer prefixes for paths
|
|
93
|
+
);
|
|
94
|
+
""")
|
|
95
|
+
|
|
96
|
+
# Print instruction to manually reindex after migration
|
|
97
|
+
print("\n------------------------------------------------------------------")
|
|
98
|
+
print("IMPORTANT: After downgrade completes, manually run the reindex command:")
|
|
99
|
+
print("basic-memory sync")
|
|
100
|
+
print("------------------------------------------------------------------\n")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Add scan watermark tracking to Project
|
|
2
|
+
|
|
3
|
+
Revision ID: e7e1f4367280
|
|
4
|
+
Revises: 9d9c1cb7d8f5
|
|
5
|
+
Create Date: 2025-10-20 16:42:46.625075
|
|
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 = "e7e1f4367280"
|
|
17
|
+
down_revision: Union[str, None] = "9d9c1cb7d8f5"
|
|
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
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
25
|
+
batch_op.add_column(sa.Column("last_scan_timestamp", sa.Float(), nullable=True))
|
|
26
|
+
batch_op.add_column(sa.Column("last_file_count", sa.Integer(), nullable=True))
|
|
27
|
+
|
|
28
|
+
# ### end Alembic commands ###
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def downgrade() -> None:
|
|
32
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
33
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
34
|
+
batch_op.drop_column("last_file_count")
|
|
35
|
+
batch_op.drop_column("last_scan_timestamp")
|
|
36
|
+
|
|
37
|
+
# ### end Alembic commands ###
|
basic_memory/api/app.py
CHANGED
|
@@ -1,45 +1,60 @@
|
|
|
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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 ConfigManager
|
|
24
|
+
from basic_memory.services.initialization import initialize_file_sync, initialize_app
|
|
14
25
|
|
|
15
|
-
from basic_memory.db import DatabaseType
|
|
16
|
-
from basic_memory.repository.search_repository import SearchRepository
|
|
17
26
|
|
|
27
|
+
@asynccontextmanager
|
|
28
|
+
async def lifespan(app: FastAPI): # pragma: no cover
|
|
29
|
+
"""Lifecycle manager for the FastAPI app. Not called in stdio mcp mode"""
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
logger.info("Running database migrations...")
|
|
22
|
-
try:
|
|
23
|
-
config = Config("alembic.ini")
|
|
24
|
-
command.upgrade(config, "head")
|
|
25
|
-
logger.info("Migrations completed successfully")
|
|
31
|
+
app_config = ConfigManager().config
|
|
32
|
+
logger.info("Starting Basic Memory API")
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
app_config.database_path, DatabaseType.FILESYSTEM
|
|
29
|
-
)
|
|
30
|
-
await SearchRepository(session_maker).init_search_index()
|
|
31
|
-
except Exception as e:
|
|
32
|
-
logger.error(f"Error running migrations: {e}")
|
|
33
|
-
raise
|
|
34
|
+
await initialize_app(app_config)
|
|
34
35
|
|
|
36
|
+
# Cache database connections in app state for performance
|
|
37
|
+
logger.info("Initializing database and caching connections...")
|
|
38
|
+
engine, session_maker = await db.get_or_create_db(app_config.database_path)
|
|
39
|
+
app.state.engine = engine
|
|
40
|
+
app.state.session_maker = session_maker
|
|
41
|
+
logger.info("Database connections cached in app state")
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
|
44
|
+
if app_config.sync_changes:
|
|
45
|
+
# start file sync task in background
|
|
46
|
+
app.state.sync_task = asyncio.create_task(initialize_file_sync(app_config))
|
|
47
|
+
else:
|
|
48
|
+
logger.info("Sync changes disabled. Skipping file sync service.")
|
|
49
|
+
|
|
50
|
+
# proceed with startup
|
|
41
51
|
yield
|
|
52
|
+
|
|
42
53
|
logger.info("Shutting down Basic Memory API")
|
|
54
|
+
if app.state.sync_task:
|
|
55
|
+
logger.info("Stopping sync...")
|
|
56
|
+
app.state.sync_task.cancel() # pyright: ignore
|
|
57
|
+
|
|
43
58
|
await db.shutdown_db()
|
|
44
59
|
|
|
45
60
|
|
|
@@ -47,20 +62,37 @@ async def lifespan(app: FastAPI): # pragma: no cover
|
|
|
47
62
|
app = FastAPI(
|
|
48
63
|
title="Basic Memory API",
|
|
49
64
|
description="Knowledge graph API for basic-memory",
|
|
50
|
-
version=
|
|
65
|
+
version=version,
|
|
51
66
|
lifespan=lifespan,
|
|
52
67
|
)
|
|
53
68
|
|
|
69
|
+
|
|
54
70
|
# Include routers
|
|
55
|
-
app.include_router(knowledge.router)
|
|
56
|
-
app.include_router(
|
|
57
|
-
app.include_router(
|
|
58
|
-
app.include_router(
|
|
71
|
+
app.include_router(knowledge.router, prefix="/{project}")
|
|
72
|
+
app.include_router(memory.router, prefix="/{project}")
|
|
73
|
+
app.include_router(resource.router, prefix="/{project}")
|
|
74
|
+
app.include_router(search.router, prefix="/{project}")
|
|
75
|
+
app.include_router(project.project_router, prefix="/{project}")
|
|
76
|
+
app.include_router(directory_router.router, prefix="/{project}")
|
|
77
|
+
app.include_router(prompt_router.router, prefix="/{project}")
|
|
78
|
+
app.include_router(importer_router.router, prefix="/{project}")
|
|
79
|
+
|
|
80
|
+
# Project resource router works accross projects
|
|
81
|
+
app.include_router(project.project_resource_router)
|
|
82
|
+
app.include_router(management.router)
|
|
83
|
+
|
|
84
|
+
# Auth routes are handled by FastMCP automatically when auth is enabled
|
|
59
85
|
|
|
60
86
|
|
|
61
87
|
@app.exception_handler(Exception)
|
|
62
88
|
async def exception_handler(request, exc): # pragma: no cover
|
|
63
89
|
logger.exception(
|
|
64
|
-
|
|
90
|
+
"API unhandled exception",
|
|
91
|
+
url=str(request.url),
|
|
92
|
+
method=request.method,
|
|
93
|
+
client=request.client.host if request.client else None,
|
|
94
|
+
path=request.url.path,
|
|
95
|
+
error_type=type(exc).__name__,
|
|
96
|
+
error=str(exc),
|
|
65
97
|
)
|
|
66
98
|
return await http_exception_handler(request, HTTPException(status_code=500, detail=str(exc)))
|
|
@@ -1,8 +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
|
|
9
|
+
from . import prompt_router as prompt
|
|
7
10
|
|
|
8
|
-
__all__ = ["knowledge", "memory", "resource", "search"]
|
|
11
|
+
__all__ = ["knowledge", "management", "memory", "project", "resource", "search", "prompt"]
|
|
@@ -0,0 +1,84 @@
|
|
|
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, response_model_exclude_none=True)
|
|
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("/structure", response_model=DirectoryNode, response_model_exclude_none=True)
|
|
35
|
+
async def get_directory_structure(
|
|
36
|
+
directory_service: DirectoryServiceDep,
|
|
37
|
+
project_id: ProjectIdDep,
|
|
38
|
+
):
|
|
39
|
+
"""Get folder structure for navigation (no files).
|
|
40
|
+
|
|
41
|
+
Optimized endpoint for folder tree navigation. Returns only directory nodes
|
|
42
|
+
without file metadata. For full tree with files, use /directory/tree.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
directory_service: Service for directory operations
|
|
46
|
+
project_id: ID of the current project
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
DirectoryNode tree containing only folders (type="directory")
|
|
50
|
+
"""
|
|
51
|
+
structure = await directory_service.get_directory_structure()
|
|
52
|
+
return structure
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/list", response_model=List[DirectoryNode], response_model_exclude_none=True)
|
|
56
|
+
async def list_directory(
|
|
57
|
+
directory_service: DirectoryServiceDep,
|
|
58
|
+
project_id: ProjectIdDep,
|
|
59
|
+
dir_name: str = Query("/", description="Directory path to list"),
|
|
60
|
+
depth: int = Query(1, ge=1, le=10, description="Recursion depth (1-10)"),
|
|
61
|
+
file_name_glob: Optional[str] = Query(
|
|
62
|
+
None, description="Glob pattern for filtering file names"
|
|
63
|
+
),
|
|
64
|
+
):
|
|
65
|
+
"""List directory contents with filtering and depth control.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
directory_service: Service for directory operations
|
|
69
|
+
project_id: ID of the current project
|
|
70
|
+
dir_name: Directory path to list (default: root "/")
|
|
71
|
+
depth: Recursion depth (1-10, default: 1 for immediate children only)
|
|
72
|
+
file_name_glob: Optional glob pattern for filtering file names (e.g., "*.md", "*meeting*")
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of DirectoryNode objects matching the criteria
|
|
76
|
+
"""
|
|
77
|
+
# Get directory listing with filtering
|
|
78
|
+
nodes = await directory_service.list_directory(
|
|
79
|
+
dir_name=dir_name,
|
|
80
|
+
depth=depth,
|
|
81
|
+
file_name_glob=file_name_glob,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
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
|
+
)
|