basic-memory 0.7.0__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 +64 -18
- 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 +166 -21
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +9 -64
- 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 +119 -4
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +43 -9
- 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 +28 -12
- basic_memory/cli/commands/import_chatgpt.py +40 -220
- basic_memory/cli/commands/import_claude_conversations.py +41 -168
- basic_memory/cli/commands/import_claude_projects.py +46 -157
- basic_memory/cli/commands/import_memory_json.py +48 -108
- basic_memory/cli/commands/mcp.py +84 -10
- basic_memory/cli/commands/project.py +876 -0
- basic_memory/cli/commands/status.py +50 -33
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +8 -7
- basic_memory/config.py +477 -23
- basic_memory/db.py +168 -17
- basic_memory/deps.py +251 -25
- 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 -23
- 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 +411 -62
- 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 +187 -25
- basic_memory/schemas/project_info.py +211 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +1 -1
- basic_memory/schemas/search.py +31 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +241 -104
- basic_memory/services/directory_service.py +295 -0
- basic_memory/services/entity_service.py +590 -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 +49 -56
- basic_memory/services/project_service.py +863 -0
- basic_memory/services/search_service.py +168 -32
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1180 -109
- 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 +383 -51
- basic_memory-0.16.1.dist-info/METADATA +493 -0
- basic_memory-0.16.1.dist-info/RECORD +148 -0
- {basic_memory-0.7.0.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 -206
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/db.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import os
|
|
2
3
|
from contextlib import asynccontextmanager
|
|
3
4
|
from enum import Enum, auto
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import AsyncGenerator, Optional
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
from basic_memory.config import ProjectConfig
|
|
8
|
+
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
|
9
9
|
from alembic import command
|
|
10
10
|
from alembic.config import Config
|
|
11
11
|
|
|
12
12
|
from loguru import logger
|
|
13
|
-
from sqlalchemy import text
|
|
13
|
+
from sqlalchemy import text, event
|
|
14
14
|
from sqlalchemy.ext.asyncio import (
|
|
15
15
|
create_async_engine,
|
|
16
16
|
async_sessionmaker,
|
|
@@ -18,12 +18,14 @@ from sqlalchemy.ext.asyncio import (
|
|
|
18
18
|
AsyncEngine,
|
|
19
19
|
async_scoped_session,
|
|
20
20
|
)
|
|
21
|
+
from sqlalchemy.pool import NullPool
|
|
21
22
|
|
|
22
23
|
from basic_memory.repository.search_repository import SearchRepository
|
|
23
24
|
|
|
24
25
|
# Module level state
|
|
25
26
|
_engine: Optional[AsyncEngine] = None
|
|
26
27
|
_session_maker: Optional[async_sessionmaker[AsyncSession]] = None
|
|
28
|
+
_migrations_completed: bool = False
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class DatabaseType(Enum):
|
|
@@ -73,32 +75,119 @@ async def scoped_session(
|
|
|
73
75
|
await factory.remove()
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
def _configure_sqlite_connection(dbapi_conn, enable_wal: bool = True) -> None:
|
|
79
|
+
"""Configure SQLite connection with WAL mode and optimizations.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
dbapi_conn: Database API connection object
|
|
83
|
+
enable_wal: Whether to enable WAL mode (should be False for in-memory databases)
|
|
84
|
+
"""
|
|
85
|
+
cursor = dbapi_conn.cursor()
|
|
86
|
+
try:
|
|
87
|
+
# Enable WAL mode for better concurrency (not supported for in-memory databases)
|
|
88
|
+
if enable_wal:
|
|
89
|
+
cursor.execute("PRAGMA journal_mode=WAL")
|
|
90
|
+
# Set busy timeout to handle locked databases
|
|
91
|
+
cursor.execute("PRAGMA busy_timeout=10000") # 10 seconds
|
|
92
|
+
# Optimize for performance
|
|
93
|
+
cursor.execute("PRAGMA synchronous=NORMAL")
|
|
94
|
+
cursor.execute("PRAGMA cache_size=-64000") # 64MB cache
|
|
95
|
+
cursor.execute("PRAGMA temp_store=MEMORY")
|
|
96
|
+
# Windows-specific optimizations
|
|
97
|
+
if os.name == "nt":
|
|
98
|
+
cursor.execute("PRAGMA locking_mode=NORMAL") # Ensure normal locking on Windows
|
|
99
|
+
except Exception as e:
|
|
100
|
+
# Log but don't fail - some PRAGMAs may not be supported
|
|
101
|
+
logger.warning(f"Failed to configure SQLite connection: {e}")
|
|
102
|
+
finally:
|
|
103
|
+
cursor.close()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _create_engine_and_session(
|
|
107
|
+
db_path: Path, db_type: DatabaseType = DatabaseType.FILESYSTEM
|
|
108
|
+
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
|
|
109
|
+
"""Internal helper to create engine and session maker."""
|
|
110
|
+
db_url = DatabaseType.get_db_url(db_path, db_type)
|
|
111
|
+
logger.debug(f"Creating engine for db_url: {db_url}")
|
|
112
|
+
|
|
113
|
+
# Configure connection args with Windows-specific settings
|
|
114
|
+
connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
|
|
115
|
+
|
|
116
|
+
# Add Windows-specific parameters to improve reliability
|
|
117
|
+
if os.name == "nt": # Windows
|
|
118
|
+
connect_args.update(
|
|
119
|
+
{
|
|
120
|
+
"timeout": 30.0, # Increase timeout to 30 seconds for Windows
|
|
121
|
+
"isolation_level": None, # Use autocommit mode
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
# Use NullPool for Windows filesystem databases to avoid connection pooling issues
|
|
125
|
+
# Important: Do NOT use NullPool for in-memory databases as it will destroy the database
|
|
126
|
+
# between connections
|
|
127
|
+
if db_type == DatabaseType.FILESYSTEM:
|
|
128
|
+
engine = create_async_engine(
|
|
129
|
+
db_url,
|
|
130
|
+
connect_args=connect_args,
|
|
131
|
+
poolclass=NullPool, # Disable connection pooling on Windows
|
|
132
|
+
echo=False,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
# In-memory databases need connection pooling to maintain state
|
|
136
|
+
engine = create_async_engine(db_url, connect_args=connect_args)
|
|
137
|
+
else:
|
|
138
|
+
engine = create_async_engine(db_url, connect_args=connect_args)
|
|
139
|
+
|
|
140
|
+
# Enable WAL mode for better concurrency and reliability
|
|
141
|
+
# Note: WAL mode is not supported for in-memory databases
|
|
142
|
+
enable_wal = db_type != DatabaseType.MEMORY
|
|
143
|
+
|
|
144
|
+
@event.listens_for(engine.sync_engine, "connect")
|
|
145
|
+
def enable_wal_mode(dbapi_conn, connection_record):
|
|
146
|
+
"""Enable WAL mode on each connection."""
|
|
147
|
+
_configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
|
|
148
|
+
|
|
149
|
+
session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
|
150
|
+
return engine, session_maker
|
|
151
|
+
|
|
152
|
+
|
|
76
153
|
async def get_or_create_db(
|
|
77
154
|
db_path: Path,
|
|
78
155
|
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
|
156
|
+
ensure_migrations: bool = True,
|
|
79
157
|
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
|
80
158
|
"""Get or create database engine and session maker."""
|
|
81
159
|
global _engine, _session_maker
|
|
82
160
|
|
|
83
161
|
if _engine is None:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
162
|
+
_engine, _session_maker = _create_engine_and_session(db_path, db_type)
|
|
163
|
+
|
|
164
|
+
# Run migrations automatically unless explicitly disabled
|
|
165
|
+
if ensure_migrations:
|
|
166
|
+
app_config = ConfigManager().config
|
|
167
|
+
await run_migrations(app_config, db_type)
|
|
168
|
+
|
|
169
|
+
# These checks should never fail since we just created the engine and session maker
|
|
170
|
+
# if they were None, but we'll check anyway for the type checker
|
|
171
|
+
if _engine is None:
|
|
172
|
+
logger.error("Failed to create database engine", db_path=str(db_path))
|
|
173
|
+
raise RuntimeError("Database engine initialization failed")
|
|
174
|
+
|
|
175
|
+
if _session_maker is None:
|
|
176
|
+
logger.error("Failed to create session maker", db_path=str(db_path))
|
|
177
|
+
raise RuntimeError("Session maker initialization failed")
|
|
88
178
|
|
|
89
|
-
assert _engine is not None # for type checker
|
|
90
|
-
assert _session_maker is not None # for type checker
|
|
91
179
|
return _engine, _session_maker
|
|
92
180
|
|
|
93
181
|
|
|
94
182
|
async def shutdown_db() -> None: # pragma: no cover
|
|
95
183
|
"""Clean up database connections."""
|
|
96
|
-
global _engine, _session_maker
|
|
184
|
+
global _engine, _session_maker, _migrations_completed
|
|
97
185
|
|
|
98
186
|
if _engine:
|
|
99
187
|
await _engine.dispose()
|
|
100
188
|
_engine = None
|
|
101
189
|
_session_maker = None
|
|
190
|
+
_migrations_completed = False
|
|
102
191
|
|
|
103
192
|
|
|
104
193
|
@asynccontextmanager
|
|
@@ -112,27 +201,79 @@ async def engine_session_factory(
|
|
|
112
201
|
for each test. For production use, use get_or_create_db() instead.
|
|
113
202
|
"""
|
|
114
203
|
|
|
115
|
-
global _engine, _session_maker
|
|
204
|
+
global _engine, _session_maker, _migrations_completed
|
|
116
205
|
|
|
117
206
|
db_url = DatabaseType.get_db_url(db_path, db_type)
|
|
118
207
|
logger.debug(f"Creating engine for db_url: {db_url}")
|
|
119
208
|
|
|
120
|
-
|
|
209
|
+
# Configure connection args with Windows-specific settings
|
|
210
|
+
connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
|
|
211
|
+
|
|
212
|
+
# Add Windows-specific parameters to improve reliability
|
|
213
|
+
if os.name == "nt": # Windows
|
|
214
|
+
connect_args.update(
|
|
215
|
+
{
|
|
216
|
+
"timeout": 30.0, # Increase timeout to 30 seconds for Windows
|
|
217
|
+
"isolation_level": None, # Use autocommit mode
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
# Use NullPool for Windows filesystem databases to avoid connection pooling issues
|
|
221
|
+
# Important: Do NOT use NullPool for in-memory databases as it will destroy the database
|
|
222
|
+
# between connections
|
|
223
|
+
if db_type == DatabaseType.FILESYSTEM:
|
|
224
|
+
_engine = create_async_engine(
|
|
225
|
+
db_url,
|
|
226
|
+
connect_args=connect_args,
|
|
227
|
+
poolclass=NullPool, # Disable connection pooling on Windows
|
|
228
|
+
echo=False,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
# In-memory databases need connection pooling to maintain state
|
|
232
|
+
_engine = create_async_engine(db_url, connect_args=connect_args)
|
|
233
|
+
else:
|
|
234
|
+
_engine = create_async_engine(db_url, connect_args=connect_args)
|
|
235
|
+
|
|
236
|
+
# Enable WAL mode for better concurrency and reliability
|
|
237
|
+
# Note: WAL mode is not supported for in-memory databases
|
|
238
|
+
enable_wal = db_type != DatabaseType.MEMORY
|
|
239
|
+
|
|
240
|
+
@event.listens_for(_engine.sync_engine, "connect")
|
|
241
|
+
def enable_wal_mode(dbapi_conn, connection_record):
|
|
242
|
+
"""Enable WAL mode on each connection."""
|
|
243
|
+
_configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
|
|
244
|
+
|
|
121
245
|
try:
|
|
122
246
|
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
|
123
247
|
|
|
124
|
-
|
|
125
|
-
|
|
248
|
+
# Verify that engine and session maker are initialized
|
|
249
|
+
if _engine is None: # pragma: no cover
|
|
250
|
+
logger.error("Database engine is None in engine_session_factory")
|
|
251
|
+
raise RuntimeError("Database engine initialization failed")
|
|
252
|
+
|
|
253
|
+
if _session_maker is None: # pragma: no cover
|
|
254
|
+
logger.error("Session maker is None in engine_session_factory")
|
|
255
|
+
raise RuntimeError("Session maker initialization failed")
|
|
256
|
+
|
|
126
257
|
yield _engine, _session_maker
|
|
127
258
|
finally:
|
|
128
259
|
if _engine:
|
|
129
260
|
await _engine.dispose()
|
|
130
261
|
_engine = None
|
|
131
262
|
_session_maker = None
|
|
263
|
+
_migrations_completed = False
|
|
132
264
|
|
|
133
265
|
|
|
134
|
-
async def run_migrations(
|
|
266
|
+
async def run_migrations(
|
|
267
|
+
app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM, force: bool = False
|
|
268
|
+
): # pragma: no cover
|
|
135
269
|
"""Run any pending alembic migrations."""
|
|
270
|
+
global _migrations_completed
|
|
271
|
+
|
|
272
|
+
# Skip if migrations already completed unless forced
|
|
273
|
+
if _migrations_completed and not force:
|
|
274
|
+
logger.debug("Migrations already completed in this session, skipping")
|
|
275
|
+
return
|
|
276
|
+
|
|
136
277
|
logger.info("Running database migrations...")
|
|
137
278
|
try:
|
|
138
279
|
# Get the absolute path to the alembic directory relative to this file
|
|
@@ -154,8 +295,18 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
|
|
|
154
295
|
command.upgrade(config, "head")
|
|
155
296
|
logger.info("Migrations completed successfully")
|
|
156
297
|
|
|
157
|
-
|
|
158
|
-
|
|
298
|
+
# Get session maker - ensure we don't trigger recursive migration calls
|
|
299
|
+
if _session_maker is None:
|
|
300
|
+
_, session_maker = _create_engine_and_session(app_config.database_path, database_type)
|
|
301
|
+
else:
|
|
302
|
+
session_maker = _session_maker
|
|
303
|
+
|
|
304
|
+
# initialize the search Index schema
|
|
305
|
+
# the project_id is not used for init_search_index, so we pass a dummy value
|
|
306
|
+
await SearchRepository(session_maker, 1).init_search_index()
|
|
307
|
+
|
|
308
|
+
# Mark migrations as completed
|
|
309
|
+
_migrations_completed = True
|
|
159
310
|
except Exception as e: # pragma: no cover
|
|
160
311
|
logger.error(f"Error running migrations: {e}")
|
|
161
312
|
raise
|
basic_memory/deps.py
CHANGED
|
@@ -1,52 +1,104 @@
|
|
|
1
1
|
"""Dependency injection functions for basic-memory services."""
|
|
2
2
|
|
|
3
3
|
from typing import Annotated
|
|
4
|
+
from loguru import logger
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
-
from fastapi import Depends
|
|
6
|
+
from fastapi import Depends, HTTPException, Path, status, Request
|
|
7
7
|
from sqlalchemy.ext.asyncio import (
|
|
8
8
|
AsyncSession,
|
|
9
9
|
AsyncEngine,
|
|
10
10
|
async_sessionmaker,
|
|
11
11
|
)
|
|
12
|
+
import pathlib
|
|
12
13
|
|
|
13
14
|
from basic_memory import db
|
|
14
|
-
from basic_memory.config import ProjectConfig,
|
|
15
|
+
from basic_memory.config import ProjectConfig, BasicMemoryConfig, ConfigManager
|
|
16
|
+
from basic_memory.importers import (
|
|
17
|
+
ChatGPTImporter,
|
|
18
|
+
ClaudeConversationsImporter,
|
|
19
|
+
ClaudeProjectsImporter,
|
|
20
|
+
MemoryJsonImporter,
|
|
21
|
+
)
|
|
15
22
|
from basic_memory.markdown import EntityParser
|
|
16
23
|
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
|
17
24
|
from basic_memory.repository.entity_repository import EntityRepository
|
|
18
25
|
from basic_memory.repository.observation_repository import ObservationRepository
|
|
26
|
+
from basic_memory.repository.project_repository import ProjectRepository
|
|
19
27
|
from basic_memory.repository.relation_repository import RelationRepository
|
|
20
28
|
from basic_memory.repository.search_repository import SearchRepository
|
|
21
|
-
from basic_memory.services import
|
|
22
|
-
EntityService,
|
|
23
|
-
)
|
|
29
|
+
from basic_memory.services import EntityService, ProjectService
|
|
24
30
|
from basic_memory.services.context_service import ContextService
|
|
31
|
+
from basic_memory.services.directory_service import DirectoryService
|
|
25
32
|
from basic_memory.services.file_service import FileService
|
|
26
33
|
from basic_memory.services.link_resolver import LinkResolver
|
|
27
34
|
from basic_memory.services.search_service import SearchService
|
|
35
|
+
from basic_memory.sync import SyncService
|
|
36
|
+
from basic_memory.utils import generate_permalink
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_app_config() -> BasicMemoryConfig: # pragma: no cover
|
|
40
|
+
app_config = ConfigManager().config
|
|
41
|
+
return app_config
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
AppConfigDep = Annotated[BasicMemoryConfig, Depends(get_app_config)] # pragma: no cover
|
|
28
45
|
|
|
29
46
|
|
|
30
47
|
## project
|
|
31
48
|
|
|
32
49
|
|
|
33
|
-
def get_project_config(
|
|
34
|
-
|
|
50
|
+
async def get_project_config(
|
|
51
|
+
project: "ProjectPathDep", project_repository: "ProjectRepositoryDep"
|
|
52
|
+
) -> ProjectConfig: # pragma: no cover
|
|
53
|
+
"""Get the current project referenced from request state.
|
|
35
54
|
|
|
55
|
+
Args:
|
|
56
|
+
request: The current request object
|
|
57
|
+
project_repository: Repository for project operations
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
Returns:
|
|
60
|
+
The resolved project config
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
HTTPException: If project is not found
|
|
64
|
+
"""
|
|
65
|
+
# Convert project name to permalink for lookup
|
|
66
|
+
project_permalink = generate_permalink(str(project))
|
|
67
|
+
project_obj = await project_repository.get_by_permalink(project_permalink)
|
|
68
|
+
if project_obj:
|
|
69
|
+
return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
|
|
38
70
|
|
|
71
|
+
# Not found
|
|
72
|
+
raise HTTPException( # pragma: no cover
|
|
73
|
+
status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # pragma: no cover
|
|
39
78
|
|
|
40
79
|
## sqlalchemy
|
|
41
80
|
|
|
42
81
|
|
|
43
82
|
async def get_engine_factory(
|
|
44
|
-
|
|
83
|
+
request: Request,
|
|
45
84
|
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
|
46
|
-
"""Get engine and session maker.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
"""Get cached engine and session maker from app state.
|
|
86
|
+
|
|
87
|
+
For API requests, returns cached connections from app.state for optimal performance.
|
|
88
|
+
For non-API contexts (CLI), falls back to direct database connection.
|
|
89
|
+
"""
|
|
90
|
+
# Try to get cached connections from app state (API context)
|
|
91
|
+
if (
|
|
92
|
+
hasattr(request, "app")
|
|
93
|
+
and hasattr(request.app.state, "engine")
|
|
94
|
+
and hasattr(request.app.state, "session_maker")
|
|
95
|
+
):
|
|
96
|
+
return request.app.state.engine, request.app.state.session_maker
|
|
97
|
+
|
|
98
|
+
# Fallback for non-API contexts (CLI)
|
|
99
|
+
logger.debug("Using fallback database connection for non-API context")
|
|
100
|
+
app_config = get_app_config()
|
|
101
|
+
engine, session_maker = await db.get_or_create_db(app_config.database_path)
|
|
50
102
|
return engine, session_maker
|
|
51
103
|
|
|
52
104
|
|
|
@@ -67,11 +119,70 @@ SessionMakerDep = Annotated[async_sessionmaker, Depends(get_session_maker)]
|
|
|
67
119
|
## repositories
|
|
68
120
|
|
|
69
121
|
|
|
122
|
+
async def get_project_repository(
|
|
123
|
+
session_maker: SessionMakerDep,
|
|
124
|
+
) -> ProjectRepository:
|
|
125
|
+
"""Get the project repository."""
|
|
126
|
+
return ProjectRepository(session_maker)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
ProjectRepositoryDep = Annotated[ProjectRepository, Depends(get_project_repository)]
|
|
130
|
+
ProjectPathDep = Annotated[str, Path()] # Use Path dependency to extract from URL
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def get_project_id(
|
|
134
|
+
project_repository: ProjectRepositoryDep,
|
|
135
|
+
project: ProjectPathDep,
|
|
136
|
+
) -> int:
|
|
137
|
+
"""Get the current project ID from request state.
|
|
138
|
+
|
|
139
|
+
When using sub-applications with /{project} mounting, the project value
|
|
140
|
+
is stored in request.state by middleware.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
request: The current request object
|
|
144
|
+
project_repository: Repository for project operations
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The resolved project ID
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
HTTPException: If project is not found
|
|
151
|
+
"""
|
|
152
|
+
# Convert project name to permalink for lookup
|
|
153
|
+
project_permalink = generate_permalink(str(project))
|
|
154
|
+
project_obj = await project_repository.get_by_permalink(project_permalink)
|
|
155
|
+
if project_obj:
|
|
156
|
+
return project_obj.id
|
|
157
|
+
|
|
158
|
+
# Try by name if permalink lookup fails
|
|
159
|
+
project_obj = await project_repository.get_by_name(str(project)) # pragma: no cover
|
|
160
|
+
if project_obj: # pragma: no cover
|
|
161
|
+
return project_obj.id
|
|
162
|
+
|
|
163
|
+
# Not found
|
|
164
|
+
raise HTTPException( # pragma: no cover
|
|
165
|
+
status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
The project_id dependency is used in the following:
|
|
171
|
+
- EntityRepository
|
|
172
|
+
- ObservationRepository
|
|
173
|
+
- RelationRepository
|
|
174
|
+
- SearchRepository
|
|
175
|
+
- ProjectInfoRepository
|
|
176
|
+
"""
|
|
177
|
+
ProjectIdDep = Annotated[int, Depends(get_project_id)]
|
|
178
|
+
|
|
179
|
+
|
|
70
180
|
async def get_entity_repository(
|
|
71
181
|
session_maker: SessionMakerDep,
|
|
182
|
+
project_id: ProjectIdDep,
|
|
72
183
|
) -> EntityRepository:
|
|
73
|
-
"""Create an EntityRepository instance."""
|
|
74
|
-
return EntityRepository(session_maker)
|
|
184
|
+
"""Create an EntityRepository instance for the current project."""
|
|
185
|
+
return EntityRepository(session_maker, project_id=project_id)
|
|
75
186
|
|
|
76
187
|
|
|
77
188
|
EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)]
|
|
@@ -79,9 +190,10 @@ EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)
|
|
|
79
190
|
|
|
80
191
|
async def get_observation_repository(
|
|
81
192
|
session_maker: SessionMakerDep,
|
|
193
|
+
project_id: ProjectIdDep,
|
|
82
194
|
) -> ObservationRepository:
|
|
83
|
-
"""Create an ObservationRepository instance."""
|
|
84
|
-
return ObservationRepository(session_maker)
|
|
195
|
+
"""Create an ObservationRepository instance for the current project."""
|
|
196
|
+
return ObservationRepository(session_maker, project_id=project_id)
|
|
85
197
|
|
|
86
198
|
|
|
87
199
|
ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observation_repository)]
|
|
@@ -89,9 +201,10 @@ ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observat
|
|
|
89
201
|
|
|
90
202
|
async def get_relation_repository(
|
|
91
203
|
session_maker: SessionMakerDep,
|
|
204
|
+
project_id: ProjectIdDep,
|
|
92
205
|
) -> RelationRepository:
|
|
93
|
-
"""Create a RelationRepository instance."""
|
|
94
|
-
return RelationRepository(session_maker)
|
|
206
|
+
"""Create a RelationRepository instance for the current project."""
|
|
207
|
+
return RelationRepository(session_maker, project_id=project_id)
|
|
95
208
|
|
|
96
209
|
|
|
97
210
|
RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)]
|
|
@@ -99,14 +212,18 @@ RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repos
|
|
|
99
212
|
|
|
100
213
|
async def get_search_repository(
|
|
101
214
|
session_maker: SessionMakerDep,
|
|
215
|
+
project_id: ProjectIdDep,
|
|
102
216
|
) -> SearchRepository:
|
|
103
|
-
"""Create a SearchRepository instance."""
|
|
104
|
-
return SearchRepository(session_maker)
|
|
217
|
+
"""Create a SearchRepository instance for the current project."""
|
|
218
|
+
return SearchRepository(session_maker, project_id=project_id)
|
|
105
219
|
|
|
106
220
|
|
|
107
221
|
SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)]
|
|
108
222
|
|
|
109
223
|
|
|
224
|
+
# ProjectInfoRepository is deprecated and will be removed in a future version.
|
|
225
|
+
# Use ProjectRepository instead, which has the same functionality plus more project-specific operations.
|
|
226
|
+
|
|
110
227
|
## services
|
|
111
228
|
|
|
112
229
|
|
|
@@ -127,7 +244,12 @@ MarkdownProcessorDep = Annotated[MarkdownProcessor, Depends(get_markdown_process
|
|
|
127
244
|
async def get_file_service(
|
|
128
245
|
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
|
129
246
|
) -> FileService:
|
|
130
|
-
|
|
247
|
+
logger.debug(
|
|
248
|
+
f"Creating FileService for project: {project_config.name}, base_path: {project_config.home}"
|
|
249
|
+
)
|
|
250
|
+
file_service = FileService(project_config.home, markdown_processor)
|
|
251
|
+
logger.debug(f"Created FileService for project: {file_service} ")
|
|
252
|
+
return file_service
|
|
131
253
|
|
|
132
254
|
|
|
133
255
|
FileServiceDep = Annotated[FileService, Depends(get_file_service)]
|
|
@@ -140,6 +262,7 @@ async def get_entity_service(
|
|
|
140
262
|
entity_parser: EntityParserDep,
|
|
141
263
|
file_service: FileServiceDep,
|
|
142
264
|
link_resolver: "LinkResolverDep",
|
|
265
|
+
app_config: AppConfigDep,
|
|
143
266
|
) -> EntityService:
|
|
144
267
|
"""Create EntityService with repository."""
|
|
145
268
|
return EntityService(
|
|
@@ -149,6 +272,7 @@ async def get_entity_service(
|
|
|
149
272
|
entity_parser=entity_parser,
|
|
150
273
|
file_service=file_service,
|
|
151
274
|
link_resolver=link_resolver,
|
|
275
|
+
app_config=app_config,
|
|
152
276
|
)
|
|
153
277
|
|
|
154
278
|
|
|
@@ -177,9 +301,111 @@ LinkResolverDep = Annotated[LinkResolver, Depends(get_link_resolver)]
|
|
|
177
301
|
|
|
178
302
|
|
|
179
303
|
async def get_context_service(
|
|
180
|
-
search_repository: SearchRepositoryDep,
|
|
304
|
+
search_repository: SearchRepositoryDep,
|
|
305
|
+
entity_repository: EntityRepositoryDep,
|
|
306
|
+
observation_repository: ObservationRepositoryDep,
|
|
181
307
|
) -> ContextService:
|
|
182
|
-
return ContextService(
|
|
308
|
+
return ContextService(
|
|
309
|
+
search_repository=search_repository,
|
|
310
|
+
entity_repository=entity_repository,
|
|
311
|
+
observation_repository=observation_repository,
|
|
312
|
+
)
|
|
183
313
|
|
|
184
314
|
|
|
185
315
|
ContextServiceDep = Annotated[ContextService, Depends(get_context_service)]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def get_sync_service(
|
|
319
|
+
app_config: AppConfigDep,
|
|
320
|
+
entity_service: EntityServiceDep,
|
|
321
|
+
entity_parser: EntityParserDep,
|
|
322
|
+
entity_repository: EntityRepositoryDep,
|
|
323
|
+
relation_repository: RelationRepositoryDep,
|
|
324
|
+
project_repository: ProjectRepositoryDep,
|
|
325
|
+
search_service: SearchServiceDep,
|
|
326
|
+
file_service: FileServiceDep,
|
|
327
|
+
) -> SyncService: # pragma: no cover
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
:rtype: object
|
|
331
|
+
"""
|
|
332
|
+
return SyncService(
|
|
333
|
+
app_config=app_config,
|
|
334
|
+
entity_service=entity_service,
|
|
335
|
+
entity_parser=entity_parser,
|
|
336
|
+
entity_repository=entity_repository,
|
|
337
|
+
relation_repository=relation_repository,
|
|
338
|
+
project_repository=project_repository,
|
|
339
|
+
search_service=search_service,
|
|
340
|
+
file_service=file_service,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
SyncServiceDep = Annotated[SyncService, Depends(get_sync_service)]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def get_project_service(
|
|
348
|
+
project_repository: ProjectRepositoryDep,
|
|
349
|
+
) -> ProjectService:
|
|
350
|
+
"""Create ProjectService with repository."""
|
|
351
|
+
return ProjectService(repository=project_repository)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
ProjectServiceDep = Annotated[ProjectService, Depends(get_project_service)]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
async def get_directory_service(
|
|
358
|
+
entity_repository: EntityRepositoryDep,
|
|
359
|
+
) -> DirectoryService:
|
|
360
|
+
"""Create DirectoryService with dependencies."""
|
|
361
|
+
return DirectoryService(
|
|
362
|
+
entity_repository=entity_repository,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
DirectoryServiceDep = Annotated[DirectoryService, Depends(get_directory_service)]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# Import
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
async def get_chatgpt_importer(
|
|
373
|
+
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
|
374
|
+
) -> ChatGPTImporter:
|
|
375
|
+
"""Create ChatGPTImporter with dependencies."""
|
|
376
|
+
return ChatGPTImporter(project_config.home, markdown_processor)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
ChatGPTImporterDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer)]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def get_claude_conversations_importer(
|
|
383
|
+
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
|
384
|
+
) -> ClaudeConversationsImporter:
|
|
385
|
+
"""Create ChatGPTImporter with dependencies."""
|
|
386
|
+
return ClaudeConversationsImporter(project_config.home, markdown_processor)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
ClaudeConversationsImporterDep = Annotated[
|
|
390
|
+
ClaudeConversationsImporter, Depends(get_claude_conversations_importer)
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
async def get_claude_projects_importer(
|
|
395
|
+
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
|
396
|
+
) -> ClaudeProjectsImporter:
|
|
397
|
+
"""Create ChatGPTImporter with dependencies."""
|
|
398
|
+
return ClaudeProjectsImporter(project_config.home, markdown_processor)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
ClaudeProjectsImporterDep = Annotated[ClaudeProjectsImporter, Depends(get_claude_projects_importer)]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def get_memory_json_importer(
|
|
405
|
+
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
|
406
|
+
) -> MemoryJsonImporter:
|
|
407
|
+
"""Create ChatGPTImporter with dependencies."""
|
|
408
|
+
return MemoryJsonImporter(project_config.home, markdown_processor)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)]
|