mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
37a01b806d
- incremental sync on watch - sync non-markdown files in knowledge base - experimental `read_resource` tool for reading non-markdown files in raw form (pdf, image)
163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
"""Router for knowledge graph operations."""
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends, Query, Response
|
|
from loguru import logger
|
|
|
|
from basic_memory.deps import (
|
|
EntityServiceDep,
|
|
get_search_service,
|
|
SearchServiceDep,
|
|
LinkResolverDep,
|
|
)
|
|
from basic_memory.schemas import (
|
|
EntityListResponse,
|
|
EntityResponse,
|
|
DeleteEntitiesResponse,
|
|
DeleteEntitiesRequest,
|
|
)
|
|
from basic_memory.schemas.base import Permalink, Entity
|
|
from basic_memory.services.exceptions import EntityNotFoundError
|
|
|
|
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
|
|
|
## Create endpoints
|
|
|
|
|
|
@router.post("/entities", response_model=EntityResponse)
|
|
async def create_entity(
|
|
data: Entity,
|
|
background_tasks: BackgroundTasks,
|
|
entity_service: EntityServiceDep,
|
|
search_service: SearchServiceDep,
|
|
) -> EntityResponse:
|
|
"""Create an entity."""
|
|
logger.info(f"request: create_entity with data={data}")
|
|
|
|
entity = await entity_service.create_entity(data)
|
|
|
|
# reindex
|
|
await search_service.index_entity(entity, background_tasks=background_tasks)
|
|
result = EntityResponse.model_validate(entity)
|
|
|
|
logger.info(f"response: create_entity with result={result}")
|
|
return result
|
|
|
|
|
|
@router.put("/entities/{permalink:path}", response_model=EntityResponse)
|
|
async def create_or_update_entity(
|
|
permalink: Permalink,
|
|
data: Entity,
|
|
response: Response,
|
|
background_tasks: BackgroundTasks,
|
|
entity_service: EntityServiceDep,
|
|
search_service: SearchServiceDep,
|
|
) -> EntityResponse:
|
|
"""Create or update an entity. If entity exists, it will be updated, otherwise created."""
|
|
logger.info(f"request: create_or_update_entity with permalink={permalink}, data={data}")
|
|
|
|
# Validate permalink matches
|
|
if data.permalink != permalink:
|
|
raise HTTPException(status_code=400, detail="Entity permalink must match URL path")
|
|
|
|
# Try create_or_update operation
|
|
entity, created = await entity_service.create_or_update_entity(data)
|
|
response.status_code = 201 if created else 200
|
|
|
|
# reindex
|
|
await search_service.index_entity(entity, background_tasks=background_tasks)
|
|
result = EntityResponse.model_validate(entity)
|
|
|
|
logger.info(
|
|
f"response: create_or_update_entity with result={result}, status_code={response.status_code}"
|
|
)
|
|
return result
|
|
|
|
|
|
## Read endpoints
|
|
|
|
|
|
@router.get("/entities/{permalink:path}", response_model=EntityResponse)
|
|
async def get_entity(
|
|
entity_service: EntityServiceDep,
|
|
permalink: str,
|
|
) -> EntityResponse:
|
|
"""Get a specific entity by ID.
|
|
|
|
Args:
|
|
permalink: Entity path ID
|
|
content: If True, include full file content
|
|
:param entity_service: EntityService
|
|
"""
|
|
logger.info(f"request: get_entity with permalink={permalink}")
|
|
try:
|
|
entity = await entity_service.get_by_permalink(permalink)
|
|
result = EntityResponse.model_validate(entity)
|
|
return result
|
|
except EntityNotFoundError:
|
|
raise HTTPException(status_code=404, detail=f"Entity with {permalink} not found")
|
|
|
|
|
|
@router.get("/entities", response_model=EntityListResponse)
|
|
async def get_entities(
|
|
entity_service: EntityServiceDep,
|
|
permalink: Annotated[list[str] | None, Query()] = None,
|
|
) -> EntityListResponse:
|
|
"""Open specific entities"""
|
|
logger.info(f"request: get_entities with permalinks={permalink}")
|
|
|
|
entities = await entity_service.get_entities_by_permalinks(permalink) if permalink else []
|
|
result = EntityListResponse(
|
|
entities=[EntityResponse.model_validate(entity) for entity in entities]
|
|
)
|
|
return result
|
|
|
|
|
|
## Delete endpoints
|
|
|
|
|
|
@router.delete("/entities/{identifier:path}", response_model=DeleteEntitiesResponse)
|
|
async def delete_entity(
|
|
identifier: str,
|
|
background_tasks: BackgroundTasks,
|
|
entity_service: EntityServiceDep,
|
|
link_resolver: LinkResolverDep,
|
|
search_service=Depends(get_search_service),
|
|
) -> DeleteEntitiesResponse:
|
|
"""Delete a single entity and remove from search index."""
|
|
logger.info(f"request: delete_entity with identifier={identifier}")
|
|
|
|
entity = await link_resolver.resolve_link(identifier)
|
|
if entity is None:
|
|
return DeleteEntitiesResponse(deleted=False)
|
|
|
|
# Delete the entity
|
|
deleted = await entity_service.delete_entity(entity.permalink or entity.id)
|
|
|
|
# Remove from search index
|
|
background_tasks.add_task(search_service.delete_by_permalink, entity.permalink)
|
|
|
|
result = DeleteEntitiesResponse(deleted=deleted)
|
|
return result
|
|
|
|
|
|
@router.post("/entities/delete", response_model=DeleteEntitiesResponse)
|
|
async def delete_entities(
|
|
data: DeleteEntitiesRequest,
|
|
background_tasks: BackgroundTasks,
|
|
entity_service: EntityServiceDep,
|
|
search_service=Depends(get_search_service),
|
|
) -> DeleteEntitiesResponse:
|
|
"""Delete entities and remove from search index."""
|
|
logger.info(f"request: delete_entities with data={data}")
|
|
deleted = False
|
|
|
|
# Remove each deleted entity from search index
|
|
for permalink in data.permalinks:
|
|
deleted = await entity_service.delete_entity(permalink)
|
|
background_tasks.add_task(search_service.delete_by_permalink, permalink)
|
|
|
|
result = DeleteEntitiesResponse(deleted=deleted)
|
|
return result
|