basic-memory 0.7.0__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 +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +130 -20
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -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/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -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 +87 -20
- basic_memory/api/container.py +133 -0
- 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 +180 -23
- 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 +460 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +136 -11
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- 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 +80 -10
- basic_memory/cli/auth.py +300 -0
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +127 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +240 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +99 -0
- basic_memory/cli/commands/db.py +87 -12
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +47 -223
- basic_memory/cli/commands/import_claude_conversations.py +48 -171
- basic_memory/cli/commands/import_claude_projects.py +53 -160
- basic_memory/cli/commands/import_memory_json.py +55 -111
- basic_memory/cli/commands/mcp.py +67 -11
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +52 -34
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +14 -6
- basic_memory/config.py +580 -26
- basic_memory/db.py +285 -28
- 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 +16 -185
- basic_memory/file_utils.py +318 -54
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +100 -0
- basic_memory/importers/chatgpt_importer.py +245 -0
- basic_memory/importers/claude_conversations_importer.py +192 -0
- basic_memory/importers/claude_projects_importer.py +184 -0
- basic_memory/importers/memory_json_importer.py +128 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/entity_parser.py +182 -23
- basic_memory/markdown/markdown_processor.py +70 -7
- basic_memory/markdown/plugins.py +43 -23
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +38 -14
- basic_memory/mcp/async_client.py +135 -4
- 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 +155 -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 +61 -9
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +249 -0
- basic_memory/mcp/tools/edit_note.py +325 -0
- basic_memory/mcp/tools/list_directory.py +157 -0
- basic_memory/mcp/tools/move_note.py +549 -0
- basic_memory/mcp/tools/project_management.py +204 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +265 -0
- basic_memory/mcp/tools/recent_activity.py +528 -0
- basic_memory/mcp/tools/search.py +377 -24
- basic_memory/mcp/tools/utils.py +402 -16
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +82 -17
- basic_memory/models/project.py +93 -0
- basic_memory/models/search.py +68 -8
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +437 -8
- basic_memory/repository/observation_repository.py +36 -3
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +140 -0
- basic_memory/repository/relation_repository.py +79 -4
- basic_memory/repository/repository.py +148 -29
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +79 -268
- 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/__init__.py +22 -9
- basic_memory/schemas/base.py +131 -12
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +31 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +194 -25
- basic_memory/schemas/project_info.py +213 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +85 -28
- basic_memory/schemas/search.py +36 -35
- basic_memory/schemas/sync_report.py +72 -0
- 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/__init__.py +2 -1
- basic_memory/services/context_service.py +451 -138
- basic_memory/services/directory_service.py +310 -0
- basic_memory/services/entity_service.py +636 -71
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +402 -33
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +888 -0
- basic_memory/services/search_service.py +232 -37
- basic_memory/sync/__init__.py +4 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +1200 -109
- basic_memory/sync/watch_service.py +432 -135
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +407 -54
- basic_memory-0.17.4.dist-info/METADATA +617 -0
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.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.17.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/mcp/async_client.py
CHANGED
|
@@ -1,8 +1,139 @@
|
|
|
1
|
-
from
|
|
1
|
+
from contextlib import asynccontextmanager, AbstractAsyncContextManager
|
|
2
|
+
from typing import AsyncIterator, Callable, Optional
|
|
3
|
+
|
|
4
|
+
from httpx import ASGITransport, AsyncClient, Timeout
|
|
5
|
+
from loguru import logger
|
|
2
6
|
|
|
3
7
|
from basic_memory.api.app import app as fastapi_app
|
|
8
|
+
from basic_memory.config import ConfigManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Optional factory override for dependency injection
|
|
12
|
+
_client_factory: Optional[Callable[[], AbstractAsyncContextManager[AsyncClient]]] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_client_factory(factory: Callable[[], AbstractAsyncContextManager[AsyncClient]]) -> None:
|
|
16
|
+
"""Override the default client factory (for cloud app, testing, etc).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
factory: An async context manager that yields an AsyncClient
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
@asynccontextmanager
|
|
23
|
+
async def custom_client_factory():
|
|
24
|
+
async with AsyncClient(...) as client:
|
|
25
|
+
yield client
|
|
26
|
+
|
|
27
|
+
set_client_factory(custom_client_factory)
|
|
28
|
+
"""
|
|
29
|
+
global _client_factory
|
|
30
|
+
_client_factory = factory
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@asynccontextmanager
|
|
34
|
+
async def get_client() -> AsyncIterator[AsyncClient]:
|
|
35
|
+
"""Get an AsyncClient as a context manager.
|
|
36
|
+
|
|
37
|
+
This function provides proper resource management for HTTP clients,
|
|
38
|
+
ensuring connections are closed after use. It supports three modes:
|
|
39
|
+
|
|
40
|
+
1. **Factory injection** (cloud app, tests):
|
|
41
|
+
If a custom factory is set via set_client_factory(), use that.
|
|
42
|
+
|
|
43
|
+
2. **CLI cloud mode**:
|
|
44
|
+
When cloud_mode_enabled is True, create HTTP client with auth
|
|
45
|
+
token from CLIAuth for requests to cloud proxy endpoint.
|
|
46
|
+
|
|
47
|
+
3. **Local mode** (default):
|
|
48
|
+
Use ASGI transport for in-process requests to local FastAPI app.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
async with get_client() as client:
|
|
52
|
+
response = await client.get("/path")
|
|
53
|
+
|
|
54
|
+
Yields:
|
|
55
|
+
AsyncClient: Configured HTTP client for the current mode
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
RuntimeError: If cloud mode is enabled but user is not authenticated
|
|
59
|
+
"""
|
|
60
|
+
if _client_factory:
|
|
61
|
+
# Use injected factory (cloud app, tests)
|
|
62
|
+
async with _client_factory() as client:
|
|
63
|
+
yield client
|
|
64
|
+
else:
|
|
65
|
+
# Default: create based on config
|
|
66
|
+
config = ConfigManager().config
|
|
67
|
+
timeout = Timeout(
|
|
68
|
+
connect=10.0, # 10 seconds for connection
|
|
69
|
+
read=30.0, # 30 seconds for reading response
|
|
70
|
+
write=30.0, # 30 seconds for writing request
|
|
71
|
+
pool=30.0, # 30 seconds for connection pool
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if config.cloud_mode_enabled:
|
|
75
|
+
# CLI cloud mode: inject auth when creating client
|
|
76
|
+
from basic_memory.cli.auth import CLIAuth
|
|
77
|
+
|
|
78
|
+
auth = CLIAuth(client_id=config.cloud_client_id, authkit_domain=config.cloud_domain)
|
|
79
|
+
token = await auth.get_valid_token()
|
|
80
|
+
|
|
81
|
+
if not token:
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
"Cloud mode enabled but not authenticated. "
|
|
84
|
+
"Run 'basic-memory cloud login' first."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Auth header set ONCE at client creation
|
|
88
|
+
proxy_base_url = f"{config.cloud_host}/proxy"
|
|
89
|
+
logger.info(f"Creating HTTP client for cloud proxy at: {proxy_base_url}")
|
|
90
|
+
async with AsyncClient(
|
|
91
|
+
base_url=proxy_base_url,
|
|
92
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
93
|
+
timeout=timeout,
|
|
94
|
+
) as client:
|
|
95
|
+
yield client
|
|
96
|
+
else:
|
|
97
|
+
# Local mode: ASGI transport for in-process calls
|
|
98
|
+
# Note: ASGI transport does NOT trigger FastAPI lifespan, so no special handling needed
|
|
99
|
+
logger.info("Creating ASGI client for local Basic Memory API")
|
|
100
|
+
async with AsyncClient(
|
|
101
|
+
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
|
|
102
|
+
) as client:
|
|
103
|
+
yield client
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_client() -> AsyncClient:
|
|
107
|
+
"""Create an HTTP client based on configuration.
|
|
108
|
+
|
|
109
|
+
DEPRECATED: Use get_client() context manager instead for proper resource management.
|
|
110
|
+
|
|
111
|
+
This function is kept for backward compatibility but will be removed in a future version.
|
|
112
|
+
The returned client should be closed manually by calling await client.aclose().
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
AsyncClient configured for either local ASGI or remote proxy
|
|
116
|
+
"""
|
|
117
|
+
config_manager = ConfigManager()
|
|
118
|
+
config = config_manager.config
|
|
4
119
|
|
|
5
|
-
|
|
120
|
+
# Configure timeout for longer operations like write_note
|
|
121
|
+
# Default httpx timeout is 5 seconds which is too short for file operations
|
|
122
|
+
timeout = Timeout(
|
|
123
|
+
connect=10.0, # 10 seconds for connection
|
|
124
|
+
read=30.0, # 30 seconds for reading response
|
|
125
|
+
write=30.0, # 30 seconds for writing request
|
|
126
|
+
pool=30.0, # 30 seconds for connection pool
|
|
127
|
+
)
|
|
6
128
|
|
|
7
|
-
|
|
8
|
-
|
|
129
|
+
if config.cloud_mode_enabled:
|
|
130
|
+
# Use HTTP transport to proxy endpoint
|
|
131
|
+
proxy_base_url = f"{config.cloud_host}/proxy"
|
|
132
|
+
logger.info(f"Creating HTTP client for proxy at: {proxy_base_url}")
|
|
133
|
+
return AsyncClient(base_url=proxy_base_url, timeout=timeout)
|
|
134
|
+
else:
|
|
135
|
+
# Default: use ASGI transport for local API (development mode)
|
|
136
|
+
logger.info("Creating ASGI client for local Basic Memory API")
|
|
137
|
+
return AsyncClient(
|
|
138
|
+
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
|
|
139
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Typed internal API clients for MCP tools.
|
|
2
|
+
|
|
3
|
+
These clients encapsulate API paths, error handling, and response validation.
|
|
4
|
+
MCP tools become thin adapters that call these clients and format results.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from basic_memory.mcp.clients import KnowledgeClient, SearchClient
|
|
8
|
+
|
|
9
|
+
async with get_client() as http_client:
|
|
10
|
+
knowledge = KnowledgeClient(http_client, project_id)
|
|
11
|
+
entity = await knowledge.create_entity(entity_data)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from basic_memory.mcp.clients.knowledge import KnowledgeClient
|
|
15
|
+
from basic_memory.mcp.clients.search import SearchClient
|
|
16
|
+
from basic_memory.mcp.clients.memory import MemoryClient
|
|
17
|
+
from basic_memory.mcp.clients.directory import DirectoryClient
|
|
18
|
+
from basic_memory.mcp.clients.resource import ResourceClient
|
|
19
|
+
from basic_memory.mcp.clients.project import ProjectClient
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"KnowledgeClient",
|
|
23
|
+
"SearchClient",
|
|
24
|
+
"MemoryClient",
|
|
25
|
+
"DirectoryClient",
|
|
26
|
+
"ResourceClient",
|
|
27
|
+
"ProjectClient",
|
|
28
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Typed client for directory API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/directory/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional, Any
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DirectoryClient:
|
|
14
|
+
"""Typed client for directory listing operations.
|
|
15
|
+
|
|
16
|
+
Centralizes:
|
|
17
|
+
- API path construction for /v2/projects/{project_id}/directory/*
|
|
18
|
+
- Response validation
|
|
19
|
+
- Consistent error handling through call_* utilities
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
async with get_client() as http_client:
|
|
23
|
+
client = DirectoryClient(http_client, project_id)
|
|
24
|
+
nodes = await client.list("/", depth=2)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
28
|
+
"""Initialize the directory client.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
http_client: HTTPX AsyncClient for making requests
|
|
32
|
+
project_id: Project external_id (UUID) for API calls
|
|
33
|
+
"""
|
|
34
|
+
self.http_client = http_client
|
|
35
|
+
self.project_id = project_id
|
|
36
|
+
self._base_path = f"/v2/projects/{project_id}/directory"
|
|
37
|
+
|
|
38
|
+
async def list(
|
|
39
|
+
self,
|
|
40
|
+
dir_name: str = "/",
|
|
41
|
+
*,
|
|
42
|
+
depth: int = 1,
|
|
43
|
+
file_name_glob: Optional[str] = None,
|
|
44
|
+
) -> list[dict[str, Any]]:
|
|
45
|
+
"""List directory contents.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
dir_name: Directory path to list (default: root)
|
|
49
|
+
depth: How deep to traverse (default: 1)
|
|
50
|
+
file_name_glob: Optional glob pattern to filter files
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of directory nodes with their contents
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ToolError: If the request fails
|
|
57
|
+
"""
|
|
58
|
+
params: dict = {
|
|
59
|
+
"dir_name": dir_name,
|
|
60
|
+
"depth": depth,
|
|
61
|
+
}
|
|
62
|
+
if file_name_glob:
|
|
63
|
+
params["file_name_glob"] = file_name_glob
|
|
64
|
+
|
|
65
|
+
response = await call_get(
|
|
66
|
+
self.http_client,
|
|
67
|
+
f"{self._base_path}/list",
|
|
68
|
+
params=params,
|
|
69
|
+
)
|
|
70
|
+
return response.json()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Typed client for knowledge/entity API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/knowledge/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get, call_post, call_put, call_patch, call_delete
|
|
11
|
+
from basic_memory.schemas.response import EntityResponse, DeleteEntitiesResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KnowledgeClient:
|
|
15
|
+
"""Typed client for knowledge graph entity operations.
|
|
16
|
+
|
|
17
|
+
Centralizes:
|
|
18
|
+
- API path construction for /v2/projects/{project_id}/knowledge/*
|
|
19
|
+
- Response validation via Pydantic models
|
|
20
|
+
- Consistent error handling through call_* utilities
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
async with get_client() as http_client:
|
|
24
|
+
client = KnowledgeClient(http_client, project_id)
|
|
25
|
+
entity = await client.create_entity(entity_data)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
29
|
+
"""Initialize the knowledge client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
http_client: HTTPX AsyncClient for making requests
|
|
33
|
+
project_id: Project external_id (UUID) for API calls
|
|
34
|
+
"""
|
|
35
|
+
self.http_client = http_client
|
|
36
|
+
self.project_id = project_id
|
|
37
|
+
self._base_path = f"/v2/projects/{project_id}/knowledge"
|
|
38
|
+
|
|
39
|
+
# --- Entity CRUD Operations ---
|
|
40
|
+
|
|
41
|
+
async def create_entity(self, entity_data: dict[str, Any]) -> EntityResponse:
|
|
42
|
+
"""Create a new entity.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
entity_data: Entity data including title, content, folder, etc.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
EntityResponse with created entity details
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ToolError: If the request fails
|
|
52
|
+
"""
|
|
53
|
+
response = await call_post(
|
|
54
|
+
self.http_client,
|
|
55
|
+
f"{self._base_path}/entities",
|
|
56
|
+
json=entity_data,
|
|
57
|
+
)
|
|
58
|
+
return EntityResponse.model_validate(response.json())
|
|
59
|
+
|
|
60
|
+
async def update_entity(self, entity_id: str, entity_data: dict[str, Any]) -> EntityResponse:
|
|
61
|
+
"""Update an existing entity (full replacement).
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
entity_id: Entity external_id (UUID)
|
|
65
|
+
entity_data: Complete entity data for replacement
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
EntityResponse with updated entity details
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ToolError: If the request fails
|
|
72
|
+
"""
|
|
73
|
+
response = await call_put(
|
|
74
|
+
self.http_client,
|
|
75
|
+
f"{self._base_path}/entities/{entity_id}",
|
|
76
|
+
json=entity_data,
|
|
77
|
+
)
|
|
78
|
+
return EntityResponse.model_validate(response.json())
|
|
79
|
+
|
|
80
|
+
async def get_entity(self, entity_id: str) -> EntityResponse:
|
|
81
|
+
"""Get an entity by ID.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
entity_id: Entity external_id (UUID)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
EntityResponse with entity details
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ToolError: If the entity is not found or request fails
|
|
91
|
+
"""
|
|
92
|
+
response = await call_get(
|
|
93
|
+
self.http_client,
|
|
94
|
+
f"{self._base_path}/entities/{entity_id}",
|
|
95
|
+
)
|
|
96
|
+
return EntityResponse.model_validate(response.json())
|
|
97
|
+
|
|
98
|
+
async def patch_entity(self, entity_id: str, patch_data: dict[str, Any]) -> EntityResponse:
|
|
99
|
+
"""Partially update an entity.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
entity_id: Entity external_id (UUID)
|
|
103
|
+
patch_data: Partial entity data to update
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
EntityResponse with updated entity details
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ToolError: If the request fails
|
|
110
|
+
"""
|
|
111
|
+
response = await call_patch(
|
|
112
|
+
self.http_client,
|
|
113
|
+
f"{self._base_path}/entities/{entity_id}",
|
|
114
|
+
json=patch_data,
|
|
115
|
+
)
|
|
116
|
+
return EntityResponse.model_validate(response.json())
|
|
117
|
+
|
|
118
|
+
async def delete_entity(self, entity_id: str) -> DeleteEntitiesResponse:
|
|
119
|
+
"""Delete an entity.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
entity_id: Entity external_id (UUID)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
DeleteEntitiesResponse confirming deletion
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ToolError: If the entity is not found or request fails
|
|
129
|
+
"""
|
|
130
|
+
response = await call_delete(
|
|
131
|
+
self.http_client,
|
|
132
|
+
f"{self._base_path}/entities/{entity_id}",
|
|
133
|
+
)
|
|
134
|
+
return DeleteEntitiesResponse.model_validate(response.json())
|
|
135
|
+
|
|
136
|
+
async def move_entity(self, entity_id: str, destination_path: str) -> EntityResponse:
|
|
137
|
+
"""Move an entity to a new location.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
entity_id: Entity external_id (UUID)
|
|
141
|
+
destination_path: New file path for the entity
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
EntityResponse with updated entity details
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ToolError: If the request fails
|
|
148
|
+
"""
|
|
149
|
+
response = await call_put(
|
|
150
|
+
self.http_client,
|
|
151
|
+
f"{self._base_path}/entities/{entity_id}/move",
|
|
152
|
+
json={"destination_path": destination_path},
|
|
153
|
+
)
|
|
154
|
+
return EntityResponse.model_validate(response.json())
|
|
155
|
+
|
|
156
|
+
# --- Resolution ---
|
|
157
|
+
|
|
158
|
+
async def resolve_entity(self, identifier: str) -> str:
|
|
159
|
+
"""Resolve a string identifier to an entity external_id.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
identifier: The identifier to resolve (permalink, title, or path)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The resolved entity external_id (UUID)
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ToolError: If the identifier cannot be resolved
|
|
169
|
+
"""
|
|
170
|
+
response = await call_post(
|
|
171
|
+
self.http_client,
|
|
172
|
+
f"{self._base_path}/resolve",
|
|
173
|
+
json={"identifier": identifier},
|
|
174
|
+
)
|
|
175
|
+
data = response.json()
|
|
176
|
+
return data["external_id"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Typed client for memory/context API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/memory/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
11
|
+
from basic_memory.schemas.memory import GraphContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryClient:
|
|
15
|
+
"""Typed client for memory context operations.
|
|
16
|
+
|
|
17
|
+
Centralizes:
|
|
18
|
+
- API path construction for /v2/projects/{project_id}/memory/*
|
|
19
|
+
- Response validation via Pydantic models
|
|
20
|
+
- Consistent error handling through call_* utilities
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
async with get_client() as http_client:
|
|
24
|
+
client = MemoryClient(http_client, project_id)
|
|
25
|
+
context = await client.build_context("memory://specs/search")
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
29
|
+
"""Initialize the memory client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
http_client: HTTPX AsyncClient for making requests
|
|
33
|
+
project_id: Project external_id (UUID) for API calls
|
|
34
|
+
"""
|
|
35
|
+
self.http_client = http_client
|
|
36
|
+
self.project_id = project_id
|
|
37
|
+
self._base_path = f"/v2/projects/{project_id}/memory"
|
|
38
|
+
|
|
39
|
+
async def build_context(
|
|
40
|
+
self,
|
|
41
|
+
path: str,
|
|
42
|
+
*,
|
|
43
|
+
depth: int = 1,
|
|
44
|
+
timeframe: Optional[str] = None,
|
|
45
|
+
page: int = 1,
|
|
46
|
+
page_size: int = 10,
|
|
47
|
+
max_related: int = 10,
|
|
48
|
+
) -> GraphContext:
|
|
49
|
+
"""Build context from a memory path.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: The path to build context for (without memory:// prefix)
|
|
53
|
+
depth: How deep to traverse relations
|
|
54
|
+
timeframe: Time filter (e.g., "7d", "1 week")
|
|
55
|
+
page: Page number (1-indexed)
|
|
56
|
+
page_size: Results per page
|
|
57
|
+
max_related: Maximum related items per result
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
GraphContext with hierarchical results
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ToolError: If the request fails
|
|
64
|
+
"""
|
|
65
|
+
params: dict = {
|
|
66
|
+
"depth": depth,
|
|
67
|
+
"page": page,
|
|
68
|
+
"page_size": page_size,
|
|
69
|
+
"max_related": max_related,
|
|
70
|
+
}
|
|
71
|
+
if timeframe:
|
|
72
|
+
params["timeframe"] = timeframe
|
|
73
|
+
|
|
74
|
+
response = await call_get(
|
|
75
|
+
self.http_client,
|
|
76
|
+
f"{self._base_path}/{path}",
|
|
77
|
+
params=params,
|
|
78
|
+
)
|
|
79
|
+
return GraphContext.model_validate(response.json())
|
|
80
|
+
|
|
81
|
+
async def recent(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
timeframe: str = "7d",
|
|
85
|
+
depth: int = 1,
|
|
86
|
+
types: Optional[list[str]] = None,
|
|
87
|
+
page: int = 1,
|
|
88
|
+
page_size: int = 10,
|
|
89
|
+
) -> GraphContext:
|
|
90
|
+
"""Get recent activity.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
timeframe: Time filter (e.g., "7d", "1 week", "2 days ago")
|
|
94
|
+
depth: How deep to traverse relations
|
|
95
|
+
types: Filter by item types
|
|
96
|
+
page: Page number (1-indexed)
|
|
97
|
+
page_size: Results per page
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
GraphContext with recent activity
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ToolError: If the request fails
|
|
104
|
+
"""
|
|
105
|
+
params: dict = {
|
|
106
|
+
"timeframe": timeframe,
|
|
107
|
+
"depth": depth,
|
|
108
|
+
"page": page,
|
|
109
|
+
"page_size": page_size,
|
|
110
|
+
}
|
|
111
|
+
if types:
|
|
112
|
+
# Join types as comma-separated string if provided
|
|
113
|
+
params["type"] = ",".join(types) if isinstance(types, list) else types
|
|
114
|
+
|
|
115
|
+
response = await call_get(
|
|
116
|
+
self.http_client,
|
|
117
|
+
f"{self._base_path}/recent",
|
|
118
|
+
params=params,
|
|
119
|
+
)
|
|
120
|
+
return GraphContext.model_validate(response.json())
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Typed client for project API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates project-level endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get, call_post, call_delete
|
|
11
|
+
from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProjectClient:
|
|
15
|
+
"""Typed client for project management operations.
|
|
16
|
+
|
|
17
|
+
Centralizes:
|
|
18
|
+
- API path construction for project endpoints
|
|
19
|
+
- Response validation via Pydantic models
|
|
20
|
+
- Consistent error handling through call_* utilities
|
|
21
|
+
|
|
22
|
+
Note: This client does not require a project_id since it operates
|
|
23
|
+
across projects.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
async with get_client() as http_client:
|
|
27
|
+
client = ProjectClient(http_client)
|
|
28
|
+
projects = await client.list_projects()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, http_client: AsyncClient):
|
|
32
|
+
"""Initialize the project client.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
http_client: HTTPX AsyncClient for making requests
|
|
36
|
+
"""
|
|
37
|
+
self.http_client = http_client
|
|
38
|
+
|
|
39
|
+
async def list_projects(self) -> ProjectList:
|
|
40
|
+
"""List all available projects.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
ProjectList with all projects and default project name
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ToolError: If the request fails
|
|
47
|
+
"""
|
|
48
|
+
response = await call_get(
|
|
49
|
+
self.http_client,
|
|
50
|
+
"/projects/projects",
|
|
51
|
+
)
|
|
52
|
+
return ProjectList.model_validate(response.json())
|
|
53
|
+
|
|
54
|
+
async def create_project(self, project_data: dict[str, Any]) -> ProjectStatusResponse:
|
|
55
|
+
"""Create a new project.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project_data: Project creation data (name, path, set_default)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
ProjectStatusResponse with creation result
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ToolError: If the request fails
|
|
65
|
+
"""
|
|
66
|
+
response = await call_post(
|
|
67
|
+
self.http_client,
|
|
68
|
+
"/projects/projects",
|
|
69
|
+
json=project_data,
|
|
70
|
+
)
|
|
71
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
72
|
+
|
|
73
|
+
async def delete_project(self, project_external_id: str) -> ProjectStatusResponse:
|
|
74
|
+
"""Delete a project by its external ID.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
project_external_id: Project external ID (UUID)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ProjectStatusResponse with deletion result
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ToolError: If the request fails
|
|
84
|
+
"""
|
|
85
|
+
response = await call_delete(
|
|
86
|
+
self.http_client,
|
|
87
|
+
f"/v2/projects/{project_external_id}",
|
|
88
|
+
)
|
|
89
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Typed client for resource API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/resource/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient, Response
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceClient:
|
|
14
|
+
"""Typed client for resource operations.
|
|
15
|
+
|
|
16
|
+
Centralizes:
|
|
17
|
+
- API path construction for /v2/projects/{project_id}/resource/*
|
|
18
|
+
- Consistent error handling through call_* utilities
|
|
19
|
+
|
|
20
|
+
Note: This client returns raw Response objects for resources since they
|
|
21
|
+
may be text, images, or other binary content that needs special handling.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
async with get_client() as http_client:
|
|
25
|
+
client = ResourceClient(http_client, project_id)
|
|
26
|
+
response = await client.read(entity_id)
|
|
27
|
+
text = response.text
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
31
|
+
"""Initialize the resource client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
http_client: HTTPX AsyncClient for making requests
|
|
35
|
+
project_id: Project external_id (UUID) for API calls
|
|
36
|
+
"""
|
|
37
|
+
self.http_client = http_client
|
|
38
|
+
self.project_id = project_id
|
|
39
|
+
self._base_path = f"/v2/projects/{project_id}/resource"
|
|
40
|
+
|
|
41
|
+
async def read(
|
|
42
|
+
self,
|
|
43
|
+
entity_id: str,
|
|
44
|
+
*,
|
|
45
|
+
page: Optional[int] = None,
|
|
46
|
+
page_size: Optional[int] = None,
|
|
47
|
+
) -> Response:
|
|
48
|
+
"""Read a resource by entity ID.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
entity_id: Entity external_id (UUID)
|
|
52
|
+
page: Optional page number for paginated content
|
|
53
|
+
page_size: Optional page size for paginated content
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Raw HTTP Response (caller handles text/binary content)
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ToolError: If the resource is not found or request fails
|
|
60
|
+
"""
|
|
61
|
+
params: dict = {}
|
|
62
|
+
if page is not None:
|
|
63
|
+
params["page"] = page
|
|
64
|
+
if page_size is not None:
|
|
65
|
+
params["page_size"] = page_size
|
|
66
|
+
|
|
67
|
+
return await call_get(
|
|
68
|
+
self.http_client,
|
|
69
|
+
f"{self._base_path}/{entity_id}",
|
|
70
|
+
params=params if params else None,
|
|
71
|
+
)
|