basic-memory 0.16.1__py3-none-any.whl → 0.17.4__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 +1 -1
- basic_memory/alembic/env.py +112 -26
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +45 -24
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/knowledge_router.py +17 -5
- basic_memory/api/routers/project_router.py +68 -14
- basic_memory/api/routers/resource_router.py +37 -27
- basic_memory/api/routers/utils.py +53 -14
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +43 -7
- basic_memory/cli/auth.py +27 -4
- basic_memory/cli/commands/__init__.py +3 -1
- basic_memory/cli/commands/cloud/api_client.py +20 -5
- basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
- basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
- basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
- basic_memory/cli/commands/cloud/upload.py +10 -3
- basic_memory/cli/commands/command_utils.py +52 -4
- basic_memory/cli/commands/db.py +78 -19
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +12 -8
- basic_memory/cli/commands/import_claude_conversations.py +12 -8
- basic_memory/cli/commands/import_claude_projects.py +12 -8
- basic_memory/cli/commands/import_memory_json.py +12 -8
- basic_memory/cli/commands/mcp.py +8 -26
- basic_memory/cli/commands/project.py +22 -9
- basic_memory/cli/commands/status.py +3 -2
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +7 -0
- basic_memory/config.py +177 -77
- basic_memory/db.py +183 -77
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +14 -409
- basic_memory/file_utils.py +212 -3
- basic_memory/ignore_utils.py +5 -5
- basic_memory/importers/base.py +40 -19
- basic_memory/importers/chatgpt_importer.py +17 -4
- basic_memory/importers/claude_conversations_importer.py +27 -12
- basic_memory/importers/claude_projects_importer.py +50 -14
- basic_memory/importers/memory_json_importer.py +36 -16
- basic_memory/importers/utils.py +5 -2
- basic_memory/markdown/entity_parser.py +62 -23
- basic_memory/markdown/markdown_processor.py +67 -4
- basic_memory/markdown/plugins.py +4 -2
- basic_memory/markdown/utils.py +10 -1
- basic_memory/mcp/async_client.py +1 -0
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +47 -33
- basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
- basic_memory/mcp/prompts/recent_activity.py +2 -2
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/server.py +58 -0
- basic_memory/mcp/tools/build_context.py +14 -14
- basic_memory/mcp/tools/canvas.py +34 -12
- basic_memory/mcp/tools/chatgpt_tools.py +4 -1
- basic_memory/mcp/tools/delete_note.py +31 -7
- basic_memory/mcp/tools/edit_note.py +14 -9
- basic_memory/mcp/tools/list_directory.py +7 -17
- basic_memory/mcp/tools/move_note.py +35 -31
- basic_memory/mcp/tools/project_management.py +29 -25
- basic_memory/mcp/tools/read_content.py +13 -3
- basic_memory/mcp/tools/read_note.py +24 -14
- basic_memory/mcp/tools/recent_activity.py +32 -38
- basic_memory/mcp/tools/search.py +17 -10
- basic_memory/mcp/tools/utils.py +28 -0
- basic_memory/mcp/tools/view_note.py +2 -1
- basic_memory/mcp/tools/write_note.py +37 -14
- basic_memory/models/knowledge.py +15 -2
- basic_memory/models/project.py +7 -1
- basic_memory/models/search.py +58 -2
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/entity_repository.py +210 -3
- basic_memory/repository/observation_repository.py +1 -0
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_repository.py +38 -1
- basic_memory/repository/relation_repository.py +58 -2
- basic_memory/repository/repository.py +1 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +77 -615
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/base.py +36 -6
- basic_memory/schemas/directory.py +2 -1
- basic_memory/schemas/memory.py +9 -2
- basic_memory/schemas/project_info.py +2 -0
- basic_memory/schemas/response.py +84 -27
- basic_memory/schemas/search.py +5 -0
- basic_memory/schemas/sync_report.py +1 -1
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/context_service.py +219 -43
- basic_memory/services/directory_service.py +26 -11
- basic_memory/services/entity_service.py +68 -33
- basic_memory/services/file_service.py +131 -16
- basic_memory/services/initialization.py +51 -26
- basic_memory/services/link_resolver.py +1 -0
- basic_memory/services/project_service.py +68 -43
- basic_memory/services/search_service.py +75 -16
- basic_memory/sync/__init__.py +2 -1
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +135 -115
- basic_memory/sync/watch_service.py +32 -12
- basic_memory/telemetry.py +249 -0
- basic_memory/utils.py +96 -75
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- basic_memory-0.16.1.dist-info/RECORD +0 -148
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/db.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
|
+
import sys
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
5
|
from enum import Enum, auto
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import AsyncGenerator, Optional
|
|
7
8
|
|
|
8
|
-
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
|
9
|
+
from basic_memory.config import BasicMemoryConfig, ConfigManager, DatabaseBackend
|
|
9
10
|
from alembic import command
|
|
10
11
|
from alembic.config import Config
|
|
11
12
|
|
|
@@ -20,12 +21,27 @@ from sqlalchemy.ext.asyncio import (
|
|
|
20
21
|
)
|
|
21
22
|
from sqlalchemy.pool import NullPool
|
|
22
23
|
|
|
23
|
-
from basic_memory.repository.
|
|
24
|
+
from basic_memory.repository.postgres_search_repository import PostgresSearchRepository
|
|
25
|
+
from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository
|
|
26
|
+
|
|
27
|
+
# -----------------------------------------------------------------------------
|
|
28
|
+
# Windows event loop policy
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
# On Windows, the default ProactorEventLoop has known rough edges with aiosqlite
|
|
31
|
+
# during shutdown/teardown (threads posting results to a loop that's closing),
|
|
32
|
+
# which can manifest as:
|
|
33
|
+
# - "RuntimeError: Event loop is closed"
|
|
34
|
+
# - "IndexError: pop from an empty deque"
|
|
35
|
+
#
|
|
36
|
+
# The SelectorEventLoop doesn't support subprocess operations, so code that uses
|
|
37
|
+
# asyncio.create_subprocess_shell() (like sync_service._quick_count_files) must
|
|
38
|
+
# detect Windows and use fallback implementations.
|
|
39
|
+
if sys.platform == "win32": # pragma: no cover
|
|
40
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
24
41
|
|
|
25
42
|
# Module level state
|
|
26
43
|
_engine: Optional[AsyncEngine] = None
|
|
27
44
|
_session_maker: Optional[async_sessionmaker[AsyncSession]] = None
|
|
28
|
-
_migrations_completed: bool = False
|
|
29
45
|
|
|
30
46
|
|
|
31
47
|
class DatabaseType(Enum):
|
|
@@ -33,10 +49,41 @@ class DatabaseType(Enum):
|
|
|
33
49
|
|
|
34
50
|
MEMORY = auto()
|
|
35
51
|
FILESYSTEM = auto()
|
|
52
|
+
POSTGRES = auto()
|
|
36
53
|
|
|
37
54
|
@classmethod
|
|
38
|
-
def get_db_url(
|
|
39
|
-
""
|
|
55
|
+
def get_db_url(
|
|
56
|
+
cls, db_path: Path, db_type: "DatabaseType", config: Optional[BasicMemoryConfig] = None
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Get SQLAlchemy URL for database path.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
db_path: Path to SQLite database file (ignored for Postgres)
|
|
62
|
+
db_type: Type of database (MEMORY, FILESYSTEM, or POSTGRES)
|
|
63
|
+
config: Optional config to check for database backend and URL
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
SQLAlchemy connection URL
|
|
67
|
+
"""
|
|
68
|
+
# Load config if not provided
|
|
69
|
+
if config is None:
|
|
70
|
+
config = ConfigManager().config
|
|
71
|
+
|
|
72
|
+
# Handle explicit Postgres type
|
|
73
|
+
if db_type == cls.POSTGRES:
|
|
74
|
+
if not config.database_url:
|
|
75
|
+
raise ValueError("DATABASE_URL must be set when using Postgres backend")
|
|
76
|
+
logger.info(f"Using Postgres database: {config.database_url}")
|
|
77
|
+
return config.database_url
|
|
78
|
+
|
|
79
|
+
# Check if Postgres backend is configured (for backward compatibility)
|
|
80
|
+
if config.database_backend == DatabaseBackend.POSTGRES:
|
|
81
|
+
if not config.database_url:
|
|
82
|
+
raise ValueError("DATABASE_URL must be set when using Postgres backend")
|
|
83
|
+
logger.info(f"Using Postgres database: {config.database_url}")
|
|
84
|
+
return config.database_url
|
|
85
|
+
|
|
86
|
+
# SQLite databases
|
|
40
87
|
if db_type == cls.MEMORY:
|
|
41
88
|
logger.info("Using in-memory SQLite database")
|
|
42
89
|
return "sqlite+aiosqlite://"
|
|
@@ -64,7 +111,14 @@ async def scoped_session(
|
|
|
64
111
|
factory = get_scoped_session_factory(session_maker)
|
|
65
112
|
session = factory()
|
|
66
113
|
try:
|
|
67
|
-
|
|
114
|
+
# Only enable foreign keys for SQLite (Postgres has them enabled by default)
|
|
115
|
+
# Detect database type from session's bind (engine) dialect
|
|
116
|
+
engine = session.get_bind()
|
|
117
|
+
dialect_name = engine.dialect.name
|
|
118
|
+
|
|
119
|
+
if dialect_name == "sqlite":
|
|
120
|
+
await session.execute(text("PRAGMA foreign_keys=ON"))
|
|
121
|
+
|
|
68
122
|
yield session
|
|
69
123
|
await session.commit()
|
|
70
124
|
except Exception:
|
|
@@ -103,13 +157,16 @@ def _configure_sqlite_connection(dbapi_conn, enable_wal: bool = True) -> None:
|
|
|
103
157
|
cursor.close()
|
|
104
158
|
|
|
105
159
|
|
|
106
|
-
def
|
|
107
|
-
|
|
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}")
|
|
160
|
+
def _create_sqlite_engine(db_url: str, db_type: DatabaseType) -> AsyncEngine:
|
|
161
|
+
"""Create SQLite async engine with appropriate configuration.
|
|
112
162
|
|
|
163
|
+
Args:
|
|
164
|
+
db_url: SQLite connection URL
|
|
165
|
+
db_type: Database type (MEMORY or FILESYSTEM)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Configured async engine for SQLite
|
|
169
|
+
"""
|
|
113
170
|
# Configure connection args with Windows-specific settings
|
|
114
171
|
connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
|
|
115
172
|
|
|
@@ -146,6 +203,73 @@ def _create_engine_and_session(
|
|
|
146
203
|
"""Enable WAL mode on each connection."""
|
|
147
204
|
_configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
|
|
148
205
|
|
|
206
|
+
return engine
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _create_postgres_engine(db_url: str, config: BasicMemoryConfig) -> AsyncEngine:
|
|
210
|
+
"""Create Postgres async engine with appropriate configuration.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
db_url: Postgres connection URL (postgresql+asyncpg://...)
|
|
214
|
+
config: BasicMemoryConfig with pool settings
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Configured async engine for Postgres
|
|
218
|
+
"""
|
|
219
|
+
# Use NullPool connection issues.
|
|
220
|
+
# Assume connection pooler like PgBouncer handles connection pooling.
|
|
221
|
+
engine = create_async_engine(
|
|
222
|
+
db_url,
|
|
223
|
+
echo=False,
|
|
224
|
+
poolclass=NullPool, # No pooling - fresh connection per request
|
|
225
|
+
connect_args={
|
|
226
|
+
# Disable statement cache to avoid issues with prepared statements on reconnect
|
|
227
|
+
"statement_cache_size": 0,
|
|
228
|
+
# Allow 30s for commands (Neon cold start can take 2-5s, sometimes longer)
|
|
229
|
+
"command_timeout": 30,
|
|
230
|
+
# Allow 30s for initial connection (Neon wake-up time)
|
|
231
|
+
"timeout": 30,
|
|
232
|
+
"server_settings": {
|
|
233
|
+
"application_name": "basic-memory",
|
|
234
|
+
# Statement timeout for queries (30s to allow for cold start)
|
|
235
|
+
"statement_timeout": "30s",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
logger.debug("Created Postgres engine with NullPool (no connection pooling)")
|
|
240
|
+
|
|
241
|
+
return engine
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _create_engine_and_session(
|
|
245
|
+
db_path: Path,
|
|
246
|
+
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
|
247
|
+
config: Optional[BasicMemoryConfig] = None,
|
|
248
|
+
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
|
|
249
|
+
"""Internal helper to create engine and session maker.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
db_path: Path to database file (used for SQLite, ignored for Postgres)
|
|
253
|
+
db_type: Type of database (MEMORY, FILESYSTEM, or POSTGRES)
|
|
254
|
+
config: Optional explicit config. If not provided, reads from ConfigManager.
|
|
255
|
+
Prefer passing explicitly from composition roots.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Tuple of (engine, session_maker)
|
|
259
|
+
"""
|
|
260
|
+
# Prefer explicit parameter; fall back to ConfigManager for backwards compatibility
|
|
261
|
+
if config is None:
|
|
262
|
+
config = ConfigManager().config
|
|
263
|
+
db_url = DatabaseType.get_db_url(db_path, db_type, config)
|
|
264
|
+
logger.debug(f"Creating engine for db_url: {db_url}")
|
|
265
|
+
|
|
266
|
+
# Delegate to backend-specific engine creation
|
|
267
|
+
# Check explicit POSTGRES type first, then config setting
|
|
268
|
+
if db_type == DatabaseType.POSTGRES or config.database_backend == DatabaseBackend.POSTGRES:
|
|
269
|
+
engine = _create_postgres_engine(db_url, config)
|
|
270
|
+
else:
|
|
271
|
+
engine = _create_sqlite_engine(db_url, db_type)
|
|
272
|
+
|
|
149
273
|
session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
|
150
274
|
return engine, session_maker
|
|
151
275
|
|
|
@@ -154,17 +278,29 @@ async def get_or_create_db(
|
|
|
154
278
|
db_path: Path,
|
|
155
279
|
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
|
156
280
|
ensure_migrations: bool = True,
|
|
281
|
+
config: Optional[BasicMemoryConfig] = None,
|
|
157
282
|
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
|
158
|
-
"""Get or create database engine and session maker.
|
|
283
|
+
"""Get or create database engine and session maker.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
db_path: Path to database file
|
|
287
|
+
db_type: Type of database
|
|
288
|
+
ensure_migrations: Whether to run migrations
|
|
289
|
+
config: Optional explicit config. If not provided, reads from ConfigManager.
|
|
290
|
+
Prefer passing explicitly from composition roots.
|
|
291
|
+
"""
|
|
159
292
|
global _engine, _session_maker
|
|
160
293
|
|
|
294
|
+
# Prefer explicit parameter; fall back to ConfigManager for backwards compatibility
|
|
295
|
+
if config is None:
|
|
296
|
+
config = ConfigManager().config
|
|
297
|
+
|
|
161
298
|
if _engine is None:
|
|
162
|
-
_engine, _session_maker = _create_engine_and_session(db_path, db_type)
|
|
299
|
+
_engine, _session_maker = _create_engine_and_session(db_path, db_type, config)
|
|
163
300
|
|
|
164
301
|
# Run migrations automatically unless explicitly disabled
|
|
165
302
|
if ensure_migrations:
|
|
166
|
-
|
|
167
|
-
await run_migrations(app_config, db_type)
|
|
303
|
+
await run_migrations(config, db_type)
|
|
168
304
|
|
|
169
305
|
# These checks should never fail since we just created the engine and session maker
|
|
170
306
|
# if they were None, but we'll check anyway for the type checker
|
|
@@ -181,70 +317,37 @@ async def get_or_create_db(
|
|
|
181
317
|
|
|
182
318
|
async def shutdown_db() -> None: # pragma: no cover
|
|
183
319
|
"""Clean up database connections."""
|
|
184
|
-
global _engine, _session_maker
|
|
320
|
+
global _engine, _session_maker
|
|
185
321
|
|
|
186
322
|
if _engine:
|
|
187
323
|
await _engine.dispose()
|
|
188
324
|
_engine = None
|
|
189
325
|
_session_maker = None
|
|
190
|
-
_migrations_completed = False
|
|
191
326
|
|
|
192
327
|
|
|
193
328
|
@asynccontextmanager
|
|
194
329
|
async def engine_session_factory(
|
|
195
330
|
db_path: Path,
|
|
196
331
|
db_type: DatabaseType = DatabaseType.MEMORY,
|
|
332
|
+
config: Optional[BasicMemoryConfig] = None,
|
|
197
333
|
) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
|
|
198
334
|
"""Create engine and session factory.
|
|
199
335
|
|
|
200
336
|
Note: This is primarily used for testing where we want a fresh database
|
|
201
337
|
for each test. For production use, use get_or_create_db() instead.
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
global _engine, _session_maker, _migrations_completed
|
|
205
338
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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)
|
|
339
|
+
Args:
|
|
340
|
+
db_path: Path to database file
|
|
341
|
+
db_type: Type of database
|
|
342
|
+
config: Optional explicit config. If not provided, reads from ConfigManager.
|
|
343
|
+
"""
|
|
235
344
|
|
|
236
|
-
|
|
237
|
-
# Note: WAL mode is not supported for in-memory databases
|
|
238
|
-
enable_wal = db_type != DatabaseType.MEMORY
|
|
345
|
+
global _engine, _session_maker
|
|
239
346
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"""Enable WAL mode on each connection."""
|
|
243
|
-
_configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
|
|
347
|
+
# Use the same helper function as production code
|
|
348
|
+
_engine, _session_maker = _create_engine_and_session(db_path, db_type, config)
|
|
244
349
|
|
|
245
350
|
try:
|
|
246
|
-
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
|
247
|
-
|
|
248
351
|
# Verify that engine and session maker are initialized
|
|
249
352
|
if _engine is None: # pragma: no cover
|
|
250
353
|
logger.error("Database engine is None in engine_session_factory")
|
|
@@ -260,20 +363,16 @@ async def engine_session_factory(
|
|
|
260
363
|
await _engine.dispose()
|
|
261
364
|
_engine = None
|
|
262
365
|
_session_maker = None
|
|
263
|
-
_migrations_completed = False
|
|
264
366
|
|
|
265
367
|
|
|
266
368
|
async def run_migrations(
|
|
267
|
-
app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM
|
|
369
|
+
app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM
|
|
268
370
|
): # pragma: no cover
|
|
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
|
|
371
|
+
"""Run any pending alembic migrations.
|
|
276
372
|
|
|
373
|
+
Note: Alembic tracks which migrations have been applied via the alembic_version table,
|
|
374
|
+
so it's safe to call this multiple times - it will only run pending migrations.
|
|
375
|
+
"""
|
|
277
376
|
logger.info("Running database migrations...")
|
|
278
377
|
try:
|
|
279
378
|
# Get the absolute path to the alembic directory relative to this file
|
|
@@ -288,9 +387,11 @@ async def run_migrations(
|
|
|
288
387
|
)
|
|
289
388
|
config.set_main_option("timezone", "UTC")
|
|
290
389
|
config.set_main_option("revision_environment", "false")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
390
|
+
|
|
391
|
+
# Get the correct database URL based on backend configuration
|
|
392
|
+
# No URL conversion needed - env.py now handles both async and sync engines
|
|
393
|
+
db_url = DatabaseType.get_db_url(app_config.database_path, database_type, app_config)
|
|
394
|
+
config.set_main_option("sqlalchemy.url", db_url)
|
|
294
395
|
|
|
295
396
|
command.upgrade(config, "head")
|
|
296
397
|
logger.info("Migrations completed successfully")
|
|
@@ -301,12 +402,17 @@ async def run_migrations(
|
|
|
301
402
|
else:
|
|
302
403
|
session_maker = _session_maker
|
|
303
404
|
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
405
|
+
# Initialize the search index schema
|
|
406
|
+
# For SQLite: Create FTS5 virtual table
|
|
407
|
+
# For Postgres: No-op (tsvector column added by migrations)
|
|
408
|
+
# The project_id is not used for init_search_index, so we pass a dummy value
|
|
409
|
+
if (
|
|
410
|
+
database_type == DatabaseType.POSTGRES
|
|
411
|
+
or app_config.database_backend == DatabaseBackend.POSTGRES
|
|
412
|
+
):
|
|
413
|
+
await PostgresSearchRepository(session_maker, 1).init_search_index()
|
|
414
|
+
else:
|
|
415
|
+
await SQLiteSearchRepository(session_maker, 1).init_search_index()
|
|
310
416
|
except Exception as e: # pragma: no cover
|
|
311
417
|
logger.error(f"Error running migrations: {e}")
|
|
312
418
|
raise
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Dependency injection for basic-memory.
|
|
2
|
+
|
|
3
|
+
This package provides FastAPI dependencies organized by feature:
|
|
4
|
+
- config: Application configuration
|
|
5
|
+
- db: Database/session management
|
|
6
|
+
- projects: Project resolution and config
|
|
7
|
+
- repositories: Data access layer
|
|
8
|
+
- services: Business logic layer
|
|
9
|
+
- importers: Import functionality
|
|
10
|
+
|
|
11
|
+
For backwards compatibility, all dependencies are re-exported from this module.
|
|
12
|
+
New code should import from specific submodules to reduce coupling.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Re-export everything for backwards compatibility
|
|
16
|
+
# Eventually, callers should import from specific submodules
|
|
17
|
+
|
|
18
|
+
from basic_memory.deps.config import (
|
|
19
|
+
get_app_config,
|
|
20
|
+
AppConfigDep,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from basic_memory.deps.db import (
|
|
24
|
+
get_engine_factory,
|
|
25
|
+
EngineFactoryDep,
|
|
26
|
+
get_session_maker,
|
|
27
|
+
SessionMakerDep,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from basic_memory.deps.projects import (
|
|
31
|
+
get_project_repository,
|
|
32
|
+
ProjectRepositoryDep,
|
|
33
|
+
ProjectPathDep,
|
|
34
|
+
get_project_id,
|
|
35
|
+
ProjectIdDep,
|
|
36
|
+
get_project_config,
|
|
37
|
+
ProjectConfigDep,
|
|
38
|
+
validate_project_id,
|
|
39
|
+
ProjectIdPathDep,
|
|
40
|
+
get_project_config_v2,
|
|
41
|
+
ProjectConfigV2Dep,
|
|
42
|
+
validate_project_external_id,
|
|
43
|
+
ProjectExternalIdPathDep,
|
|
44
|
+
get_project_config_v2_external,
|
|
45
|
+
ProjectConfigV2ExternalDep,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from basic_memory.deps.repositories import (
|
|
49
|
+
get_entity_repository,
|
|
50
|
+
EntityRepositoryDep,
|
|
51
|
+
get_entity_repository_v2,
|
|
52
|
+
EntityRepositoryV2Dep,
|
|
53
|
+
get_entity_repository_v2_external,
|
|
54
|
+
EntityRepositoryV2ExternalDep,
|
|
55
|
+
get_observation_repository,
|
|
56
|
+
ObservationRepositoryDep,
|
|
57
|
+
get_observation_repository_v2,
|
|
58
|
+
ObservationRepositoryV2Dep,
|
|
59
|
+
get_observation_repository_v2_external,
|
|
60
|
+
ObservationRepositoryV2ExternalDep,
|
|
61
|
+
get_relation_repository,
|
|
62
|
+
RelationRepositoryDep,
|
|
63
|
+
get_relation_repository_v2,
|
|
64
|
+
RelationRepositoryV2Dep,
|
|
65
|
+
get_relation_repository_v2_external,
|
|
66
|
+
RelationRepositoryV2ExternalDep,
|
|
67
|
+
get_search_repository,
|
|
68
|
+
SearchRepositoryDep,
|
|
69
|
+
get_search_repository_v2,
|
|
70
|
+
SearchRepositoryV2Dep,
|
|
71
|
+
get_search_repository_v2_external,
|
|
72
|
+
SearchRepositoryV2ExternalDep,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
from basic_memory.deps.services import (
|
|
76
|
+
get_entity_parser,
|
|
77
|
+
EntityParserDep,
|
|
78
|
+
get_entity_parser_v2,
|
|
79
|
+
EntityParserV2Dep,
|
|
80
|
+
get_entity_parser_v2_external,
|
|
81
|
+
EntityParserV2ExternalDep,
|
|
82
|
+
get_markdown_processor,
|
|
83
|
+
MarkdownProcessorDep,
|
|
84
|
+
get_markdown_processor_v2,
|
|
85
|
+
MarkdownProcessorV2Dep,
|
|
86
|
+
get_markdown_processor_v2_external,
|
|
87
|
+
MarkdownProcessorV2ExternalDep,
|
|
88
|
+
get_file_service,
|
|
89
|
+
FileServiceDep,
|
|
90
|
+
get_file_service_v2,
|
|
91
|
+
FileServiceV2Dep,
|
|
92
|
+
get_file_service_v2_external,
|
|
93
|
+
FileServiceV2ExternalDep,
|
|
94
|
+
get_search_service,
|
|
95
|
+
SearchServiceDep,
|
|
96
|
+
get_search_service_v2,
|
|
97
|
+
SearchServiceV2Dep,
|
|
98
|
+
get_search_service_v2_external,
|
|
99
|
+
SearchServiceV2ExternalDep,
|
|
100
|
+
get_link_resolver,
|
|
101
|
+
LinkResolverDep,
|
|
102
|
+
get_link_resolver_v2,
|
|
103
|
+
LinkResolverV2Dep,
|
|
104
|
+
get_link_resolver_v2_external,
|
|
105
|
+
LinkResolverV2ExternalDep,
|
|
106
|
+
get_entity_service,
|
|
107
|
+
EntityServiceDep,
|
|
108
|
+
get_entity_service_v2,
|
|
109
|
+
EntityServiceV2Dep,
|
|
110
|
+
get_entity_service_v2_external,
|
|
111
|
+
EntityServiceV2ExternalDep,
|
|
112
|
+
get_context_service,
|
|
113
|
+
ContextServiceDep,
|
|
114
|
+
get_context_service_v2,
|
|
115
|
+
ContextServiceV2Dep,
|
|
116
|
+
get_context_service_v2_external,
|
|
117
|
+
ContextServiceV2ExternalDep,
|
|
118
|
+
get_sync_service,
|
|
119
|
+
SyncServiceDep,
|
|
120
|
+
get_sync_service_v2,
|
|
121
|
+
SyncServiceV2Dep,
|
|
122
|
+
get_sync_service_v2_external,
|
|
123
|
+
SyncServiceV2ExternalDep,
|
|
124
|
+
get_project_service,
|
|
125
|
+
ProjectServiceDep,
|
|
126
|
+
get_directory_service,
|
|
127
|
+
DirectoryServiceDep,
|
|
128
|
+
get_directory_service_v2,
|
|
129
|
+
DirectoryServiceV2Dep,
|
|
130
|
+
get_directory_service_v2_external,
|
|
131
|
+
DirectoryServiceV2ExternalDep,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
from basic_memory.deps.importers import (
|
|
135
|
+
get_chatgpt_importer,
|
|
136
|
+
ChatGPTImporterDep,
|
|
137
|
+
get_chatgpt_importer_v2,
|
|
138
|
+
ChatGPTImporterV2Dep,
|
|
139
|
+
get_chatgpt_importer_v2_external,
|
|
140
|
+
ChatGPTImporterV2ExternalDep,
|
|
141
|
+
get_claude_conversations_importer,
|
|
142
|
+
ClaudeConversationsImporterDep,
|
|
143
|
+
get_claude_conversations_importer_v2,
|
|
144
|
+
ClaudeConversationsImporterV2Dep,
|
|
145
|
+
get_claude_conversations_importer_v2_external,
|
|
146
|
+
ClaudeConversationsImporterV2ExternalDep,
|
|
147
|
+
get_claude_projects_importer,
|
|
148
|
+
ClaudeProjectsImporterDep,
|
|
149
|
+
get_claude_projects_importer_v2,
|
|
150
|
+
ClaudeProjectsImporterV2Dep,
|
|
151
|
+
get_claude_projects_importer_v2_external,
|
|
152
|
+
ClaudeProjectsImporterV2ExternalDep,
|
|
153
|
+
get_memory_json_importer,
|
|
154
|
+
MemoryJsonImporterDep,
|
|
155
|
+
get_memory_json_importer_v2,
|
|
156
|
+
MemoryJsonImporterV2Dep,
|
|
157
|
+
get_memory_json_importer_v2_external,
|
|
158
|
+
MemoryJsonImporterV2ExternalDep,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
__all__ = [
|
|
162
|
+
# Config
|
|
163
|
+
"get_app_config",
|
|
164
|
+
"AppConfigDep",
|
|
165
|
+
# Database
|
|
166
|
+
"get_engine_factory",
|
|
167
|
+
"EngineFactoryDep",
|
|
168
|
+
"get_session_maker",
|
|
169
|
+
"SessionMakerDep",
|
|
170
|
+
# Projects
|
|
171
|
+
"get_project_repository",
|
|
172
|
+
"ProjectRepositoryDep",
|
|
173
|
+
"ProjectPathDep",
|
|
174
|
+
"get_project_id",
|
|
175
|
+
"ProjectIdDep",
|
|
176
|
+
"get_project_config",
|
|
177
|
+
"ProjectConfigDep",
|
|
178
|
+
"validate_project_id",
|
|
179
|
+
"ProjectIdPathDep",
|
|
180
|
+
"get_project_config_v2",
|
|
181
|
+
"ProjectConfigV2Dep",
|
|
182
|
+
"validate_project_external_id",
|
|
183
|
+
"ProjectExternalIdPathDep",
|
|
184
|
+
"get_project_config_v2_external",
|
|
185
|
+
"ProjectConfigV2ExternalDep",
|
|
186
|
+
# Repositories
|
|
187
|
+
"get_entity_repository",
|
|
188
|
+
"EntityRepositoryDep",
|
|
189
|
+
"get_entity_repository_v2",
|
|
190
|
+
"EntityRepositoryV2Dep",
|
|
191
|
+
"get_entity_repository_v2_external",
|
|
192
|
+
"EntityRepositoryV2ExternalDep",
|
|
193
|
+
"get_observation_repository",
|
|
194
|
+
"ObservationRepositoryDep",
|
|
195
|
+
"get_observation_repository_v2",
|
|
196
|
+
"ObservationRepositoryV2Dep",
|
|
197
|
+
"get_observation_repository_v2_external",
|
|
198
|
+
"ObservationRepositoryV2ExternalDep",
|
|
199
|
+
"get_relation_repository",
|
|
200
|
+
"RelationRepositoryDep",
|
|
201
|
+
"get_relation_repository_v2",
|
|
202
|
+
"RelationRepositoryV2Dep",
|
|
203
|
+
"get_relation_repository_v2_external",
|
|
204
|
+
"RelationRepositoryV2ExternalDep",
|
|
205
|
+
"get_search_repository",
|
|
206
|
+
"SearchRepositoryDep",
|
|
207
|
+
"get_search_repository_v2",
|
|
208
|
+
"SearchRepositoryV2Dep",
|
|
209
|
+
"get_search_repository_v2_external",
|
|
210
|
+
"SearchRepositoryV2ExternalDep",
|
|
211
|
+
# Services
|
|
212
|
+
"get_entity_parser",
|
|
213
|
+
"EntityParserDep",
|
|
214
|
+
"get_entity_parser_v2",
|
|
215
|
+
"EntityParserV2Dep",
|
|
216
|
+
"get_entity_parser_v2_external",
|
|
217
|
+
"EntityParserV2ExternalDep",
|
|
218
|
+
"get_markdown_processor",
|
|
219
|
+
"MarkdownProcessorDep",
|
|
220
|
+
"get_markdown_processor_v2",
|
|
221
|
+
"MarkdownProcessorV2Dep",
|
|
222
|
+
"get_markdown_processor_v2_external",
|
|
223
|
+
"MarkdownProcessorV2ExternalDep",
|
|
224
|
+
"get_file_service",
|
|
225
|
+
"FileServiceDep",
|
|
226
|
+
"get_file_service_v2",
|
|
227
|
+
"FileServiceV2Dep",
|
|
228
|
+
"get_file_service_v2_external",
|
|
229
|
+
"FileServiceV2ExternalDep",
|
|
230
|
+
"get_search_service",
|
|
231
|
+
"SearchServiceDep",
|
|
232
|
+
"get_search_service_v2",
|
|
233
|
+
"SearchServiceV2Dep",
|
|
234
|
+
"get_search_service_v2_external",
|
|
235
|
+
"SearchServiceV2ExternalDep",
|
|
236
|
+
"get_link_resolver",
|
|
237
|
+
"LinkResolverDep",
|
|
238
|
+
"get_link_resolver_v2",
|
|
239
|
+
"LinkResolverV2Dep",
|
|
240
|
+
"get_link_resolver_v2_external",
|
|
241
|
+
"LinkResolverV2ExternalDep",
|
|
242
|
+
"get_entity_service",
|
|
243
|
+
"EntityServiceDep",
|
|
244
|
+
"get_entity_service_v2",
|
|
245
|
+
"EntityServiceV2Dep",
|
|
246
|
+
"get_entity_service_v2_external",
|
|
247
|
+
"EntityServiceV2ExternalDep",
|
|
248
|
+
"get_context_service",
|
|
249
|
+
"ContextServiceDep",
|
|
250
|
+
"get_context_service_v2",
|
|
251
|
+
"ContextServiceV2Dep",
|
|
252
|
+
"get_context_service_v2_external",
|
|
253
|
+
"ContextServiceV2ExternalDep",
|
|
254
|
+
"get_sync_service",
|
|
255
|
+
"SyncServiceDep",
|
|
256
|
+
"get_sync_service_v2",
|
|
257
|
+
"SyncServiceV2Dep",
|
|
258
|
+
"get_sync_service_v2_external",
|
|
259
|
+
"SyncServiceV2ExternalDep",
|
|
260
|
+
"get_project_service",
|
|
261
|
+
"ProjectServiceDep",
|
|
262
|
+
"get_directory_service",
|
|
263
|
+
"DirectoryServiceDep",
|
|
264
|
+
"get_directory_service_v2",
|
|
265
|
+
"DirectoryServiceV2Dep",
|
|
266
|
+
"get_directory_service_v2_external",
|
|
267
|
+
"DirectoryServiceV2ExternalDep",
|
|
268
|
+
# Importers
|
|
269
|
+
"get_chatgpt_importer",
|
|
270
|
+
"ChatGPTImporterDep",
|
|
271
|
+
"get_chatgpt_importer_v2",
|
|
272
|
+
"ChatGPTImporterV2Dep",
|
|
273
|
+
"get_chatgpt_importer_v2_external",
|
|
274
|
+
"ChatGPTImporterV2ExternalDep",
|
|
275
|
+
"get_claude_conversations_importer",
|
|
276
|
+
"ClaudeConversationsImporterDep",
|
|
277
|
+
"get_claude_conversations_importer_v2",
|
|
278
|
+
"ClaudeConversationsImporterV2Dep",
|
|
279
|
+
"get_claude_conversations_importer_v2_external",
|
|
280
|
+
"ClaudeConversationsImporterV2ExternalDep",
|
|
281
|
+
"get_claude_projects_importer",
|
|
282
|
+
"ClaudeProjectsImporterDep",
|
|
283
|
+
"get_claude_projects_importer_v2",
|
|
284
|
+
"ClaudeProjectsImporterV2Dep",
|
|
285
|
+
"get_claude_projects_importer_v2_external",
|
|
286
|
+
"ClaudeProjectsImporterV2ExternalDep",
|
|
287
|
+
"get_memory_json_importer",
|
|
288
|
+
"MemoryJsonImporterDep",
|
|
289
|
+
"get_memory_json_importer_v2",
|
|
290
|
+
"MemoryJsonImporterV2Dep",
|
|
291
|
+
"get_memory_json_importer_v2_external",
|
|
292
|
+
"MemoryJsonImporterV2ExternalDep",
|
|
293
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Configuration dependency injection for basic-memory.
|
|
2
|
+
|
|
3
|
+
This module provides configuration-related dependencies.
|
|
4
|
+
Note: Long-term goal is to minimize direct ConfigManager access
|
|
5
|
+
and inject config from composition roots instead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
from fastapi import Depends
|
|
11
|
+
|
|
12
|
+
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_app_config() -> BasicMemoryConfig: # pragma: no cover
|
|
16
|
+
"""Get the application configuration.
|
|
17
|
+
|
|
18
|
+
Note: This is a transitional dependency. The goal is for composition roots
|
|
19
|
+
to read ConfigManager and inject config explicitly. During migration,
|
|
20
|
+
this provides the same behavior as before.
|
|
21
|
+
"""
|
|
22
|
+
app_config = ConfigManager().config
|
|
23
|
+
return app_config
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
AppConfigDep = Annotated[BasicMemoryConfig, Depends(get_app_config)]
|