mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
98a2a3cbaf
Signed-off-by: phernandez <paul@basicmachines.co>
323 lines
11 KiB
Python
323 lines
11 KiB
Python
"""Typed client for knowledge/entity API operations.
|
|
|
|
Encapsulates all /v2/projects/{project_id}/knowledge/* endpoints.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from httpx import AsyncClient
|
|
|
|
from basic_memory import telemetry
|
|
from basic_memory.mcp.tools.utils import call_get, call_post, call_put, call_patch, call_delete
|
|
from basic_memory.schemas.response import (
|
|
EntityResponse,
|
|
DeleteEntitiesResponse,
|
|
DirectoryMoveResult,
|
|
DirectoryDeleteResult,
|
|
)
|
|
|
|
|
|
class KnowledgeClient:
|
|
"""Typed client for knowledge graph entity operations.
|
|
|
|
Centralizes:
|
|
- API path construction for /v2/projects/{project_id}/knowledge/*
|
|
- Response validation via Pydantic models
|
|
- Consistent error handling through call_* utilities
|
|
|
|
Usage:
|
|
async with get_client() as http_client:
|
|
client = KnowledgeClient(http_client, project_id)
|
|
entity = await client.create_entity(entity_data)
|
|
"""
|
|
|
|
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
"""Initialize the knowledge client.
|
|
|
|
Args:
|
|
http_client: HTTPX AsyncClient for making requests
|
|
project_id: Project external_id (UUID) for API calls
|
|
"""
|
|
self.http_client = http_client
|
|
self.project_id = project_id
|
|
self._base_path = f"/v2/projects/{project_id}/knowledge"
|
|
|
|
# --- Entity CRUD Operations ---
|
|
|
|
async def create_entity(
|
|
self, entity_data: dict[str, Any], *, fast: bool | None = None
|
|
) -> EntityResponse:
|
|
"""Create a new entity.
|
|
|
|
Args:
|
|
entity_data: Entity data including title, content, folder, etc.
|
|
|
|
Returns:
|
|
EntityResponse with created entity details
|
|
|
|
Raises:
|
|
ToolError: If the request fails
|
|
"""
|
|
params = {"fast": fast} if fast is not None else None
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.create_entity",
|
|
client_name="knowledge",
|
|
operation="create_entity",
|
|
fast=fast,
|
|
):
|
|
response = await call_post(
|
|
self.http_client,
|
|
f"{self._base_path}/entities",
|
|
json=entity_data,
|
|
params=params,
|
|
client_name="knowledge",
|
|
operation="create_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/entities",
|
|
)
|
|
return EntityResponse.model_validate(response.json())
|
|
|
|
async def update_entity(
|
|
self,
|
|
entity_id: str,
|
|
entity_data: dict[str, Any],
|
|
*,
|
|
fast: bool | None = None,
|
|
) -> EntityResponse:
|
|
"""Update an existing entity (full replacement).
|
|
|
|
Args:
|
|
entity_id: Entity external_id (UUID)
|
|
entity_data: Complete entity data for replacement
|
|
|
|
Returns:
|
|
EntityResponse with updated entity details
|
|
|
|
Raises:
|
|
ToolError: If the request fails
|
|
"""
|
|
params = {"fast": fast} if fast is not None else None
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.update_entity",
|
|
client_name="knowledge",
|
|
operation="update_entity",
|
|
fast=fast,
|
|
):
|
|
response = await call_put(
|
|
self.http_client,
|
|
f"{self._base_path}/entities/{entity_id}",
|
|
json=entity_data,
|
|
params=params,
|
|
client_name="knowledge",
|
|
operation="update_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/entities/{entity_id}",
|
|
)
|
|
return EntityResponse.model_validate(response.json())
|
|
|
|
async def get_entity(self, entity_id: str) -> EntityResponse:
|
|
"""Get an entity by ID.
|
|
|
|
Args:
|
|
entity_id: Entity external_id (UUID)
|
|
|
|
Returns:
|
|
EntityResponse with entity details
|
|
|
|
Raises:
|
|
ToolError: If the entity is not found or request fails
|
|
"""
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.get_entity",
|
|
client_name="knowledge",
|
|
operation="get_entity",
|
|
):
|
|
response = await call_get(
|
|
self.http_client,
|
|
f"{self._base_path}/entities/{entity_id}",
|
|
client_name="knowledge",
|
|
operation="get_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/entities/{entity_id}",
|
|
)
|
|
return EntityResponse.model_validate(response.json())
|
|
|
|
async def patch_entity(
|
|
self,
|
|
entity_id: str,
|
|
patch_data: dict[str, Any],
|
|
*,
|
|
fast: bool | None = None,
|
|
) -> EntityResponse:
|
|
"""Partially update an entity.
|
|
|
|
Args:
|
|
entity_id: Entity external_id (UUID)
|
|
patch_data: Partial entity data to update
|
|
|
|
Returns:
|
|
EntityResponse with updated entity details
|
|
|
|
Raises:
|
|
ToolError: If the request fails
|
|
"""
|
|
params = {"fast": fast} if fast is not None else None
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.patch_entity",
|
|
client_name="knowledge",
|
|
operation="patch_entity",
|
|
fast=fast,
|
|
):
|
|
response = await call_patch(
|
|
self.http_client,
|
|
f"{self._base_path}/entities/{entity_id}",
|
|
json=patch_data,
|
|
params=params,
|
|
client_name="knowledge",
|
|
operation="patch_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/entities/{entity_id}",
|
|
)
|
|
return EntityResponse.model_validate(response.json())
|
|
|
|
async def delete_entity(self, entity_id: str) -> DeleteEntitiesResponse:
|
|
"""Delete an entity.
|
|
|
|
Args:
|
|
entity_id: Entity external_id (UUID)
|
|
|
|
Returns:
|
|
DeleteEntitiesResponse confirming deletion
|
|
|
|
Raises:
|
|
ToolError: If the entity is not found or request fails
|
|
"""
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.delete_entity",
|
|
client_name="knowledge",
|
|
operation="delete_entity",
|
|
):
|
|
response = await call_delete(
|
|
self.http_client,
|
|
f"{self._base_path}/entities/{entity_id}",
|
|
client_name="knowledge",
|
|
operation="delete_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/entities/{entity_id}",
|
|
)
|
|
return DeleteEntitiesResponse.model_validate(response.json())
|
|
|
|
async def move_entity(self, entity_id: str, destination_path: str) -> EntityResponse:
|
|
"""Move an entity to a new location.
|
|
|
|
Args:
|
|
entity_id: Entity external_id (UUID)
|
|
destination_path: New file path for the entity
|
|
|
|
Returns:
|
|
EntityResponse with updated entity details
|
|
|
|
Raises:
|
|
ToolError: If the request fails
|
|
"""
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.move_entity",
|
|
client_name="knowledge",
|
|
operation="move_entity",
|
|
):
|
|
response = await call_put(
|
|
self.http_client,
|
|
f"{self._base_path}/entities/{entity_id}/move",
|
|
json={"destination_path": destination_path},
|
|
client_name="knowledge",
|
|
operation="move_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/entities/{entity_id}/move",
|
|
)
|
|
return EntityResponse.model_validate(response.json())
|
|
|
|
async def move_directory(
|
|
self, source_directory: str, destination_directory: str
|
|
) -> DirectoryMoveResult:
|
|
"""Move all entities in a directory to a new location.
|
|
|
|
Args:
|
|
source_directory: Source directory path (relative to project root)
|
|
destination_directory: Destination directory path (relative to project root)
|
|
|
|
Returns:
|
|
DirectoryMoveResult with counts and details of moved files
|
|
|
|
Raises:
|
|
ToolError: If the request fails
|
|
"""
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.move_directory",
|
|
client_name="knowledge",
|
|
operation="move_directory",
|
|
):
|
|
response = await call_post(
|
|
self.http_client,
|
|
f"{self._base_path}/move-directory",
|
|
json={
|
|
"source_directory": source_directory,
|
|
"destination_directory": destination_directory,
|
|
},
|
|
client_name="knowledge",
|
|
operation="move_directory",
|
|
path_template="/v2/projects/{project_id}/knowledge/move-directory",
|
|
)
|
|
return DirectoryMoveResult.model_validate(response.json())
|
|
|
|
async def delete_directory(self, directory: str) -> DirectoryDeleteResult:
|
|
"""Delete all entities in a directory.
|
|
|
|
Args:
|
|
directory: Directory path to delete (relative to project root)
|
|
|
|
Returns:
|
|
DirectoryDeleteResult with counts and details of deleted files
|
|
|
|
Raises:
|
|
ToolError: If the request fails
|
|
"""
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.delete_directory",
|
|
client_name="knowledge",
|
|
operation="delete_directory",
|
|
):
|
|
response = await call_post(
|
|
self.http_client,
|
|
f"{self._base_path}/delete-directory",
|
|
json={"directory": directory},
|
|
client_name="knowledge",
|
|
operation="delete_directory",
|
|
path_template="/v2/projects/{project_id}/knowledge/delete-directory",
|
|
)
|
|
return DirectoryDeleteResult.model_validate(response.json())
|
|
|
|
# --- Resolution ---
|
|
|
|
async def resolve_entity(self, identifier: str, *, strict: bool = False) -> str:
|
|
"""Resolve a string identifier to an entity external_id.
|
|
|
|
Args:
|
|
identifier: The identifier to resolve (permalink, title, or path)
|
|
strict: If True, require exact matching (no fuzzy fallback)
|
|
|
|
Returns:
|
|
The resolved entity external_id (UUID)
|
|
|
|
Raises:
|
|
ToolError: If the identifier cannot be resolved
|
|
"""
|
|
with telemetry.scope(
|
|
"mcp.client.knowledge.resolve_entity",
|
|
client_name="knowledge",
|
|
operation="resolve_entity",
|
|
):
|
|
response = await call_post(
|
|
self.http_client,
|
|
f"{self._base_path}/resolve",
|
|
json={"identifier": identifier, "strict": strict},
|
|
client_name="knowledge",
|
|
operation="resolve_entity",
|
|
path_template="/v2/projects/{project_id}/knowledge/resolve",
|
|
)
|
|
data = response.json()
|
|
return data["external_id"]
|