mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
change /knowledge endpoints to use ids
This commit is contained in:
@@ -15,6 +15,7 @@ def entity_model(entity):
|
||||
model = EntityModel(
|
||||
name=entity.name,
|
||||
entity_type=entity.entity_type,
|
||||
path_id=entity.path_id,
|
||||
description=entity.description,
|
||||
observations=[Observation(content=observation) for observation in entity.observations],
|
||||
)
|
||||
@@ -44,46 +45,41 @@ class EntityService(BaseService[EntityRepository]):
|
||||
created = await self.repository.add_all([entity_model(entity) for entity in entities_in])
|
||||
return created
|
||||
|
||||
async def update_entity(self, entity_id: int, update_data: Dict[str, Any]) -> EntityModel:
|
||||
async def update_entity(self, path_id: str, update_data: Dict[str, Any]) -> EntityModel:
|
||||
"""Update an entity's fields."""
|
||||
logger.debug(f"Updating entity {entity_id} with data: {update_data}")
|
||||
updated = await self.repository.update(entity_id, update_data)
|
||||
logger.debug(f"Updating entity path_id: {path_id} with data: {update_data}")
|
||||
entity = await self.get_by_path_id(path_id)
|
||||
|
||||
updated = await self.repository.update(entity.id, update_data)
|
||||
if not updated:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_id}")
|
||||
raise EntityNotFoundError(f"Entity not found: {path_id}")
|
||||
return updated
|
||||
|
||||
async def get_entity(self, entity_id: int) -> EntityModel:
|
||||
"""Get entity by ID."""
|
||||
logger.debug(f"Getting entity by ID: {entity_id}")
|
||||
db_entity = await self.repository.find_by_id(entity_id)
|
||||
if not db_entity:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_id}")
|
||||
return db_entity
|
||||
|
||||
async def get_by_type_and_name(self, entity_type: str, name: str) -> EntityModel:
|
||||
async def get_by_path_id(self, path_id: str) -> EntityModel:
|
||||
"""Get entity by type and name combination."""
|
||||
logger.debug(f"Getting entity by type/name: {entity_type}/{name}")
|
||||
db_entity = await self.repository.get_entity_by_type_and_name(entity_type, name)
|
||||
logger.debug(f"Getting entity by path_id: {path_id}")
|
||||
db_entity = await self.repository.get_by_path_id(path_id)
|
||||
if not db_entity:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_type}/{name}")
|
||||
raise EntityNotFoundError(f"Entity not found: {path_id}")
|
||||
return db_entity
|
||||
|
||||
async def get_all(self) -> Sequence[EntityModel]:
|
||||
"""Get all entities."""
|
||||
return await self.repository.find_all()
|
||||
|
||||
async def delete_entity(self, entity_id: int) -> bool:
|
||||
async def delete_entity(self, path_id: str) -> bool:
|
||||
"""Delete entity from database."""
|
||||
logger.debug(f"Deleting entity: {entity_id}")
|
||||
return await self.repository.delete(entity_id)
|
||||
logger.debug(f"Deleting entity path_id: {path_id}")
|
||||
entity = await self.get_by_path_id(path_id)
|
||||
return await self.repository.delete(entity.id)
|
||||
|
||||
async def open_nodes(self, entity_ids: List[int]) -> Sequence[EntityModel]:
|
||||
async def open_nodes(self, path_ids: List[str]) -> Sequence[EntityModel]:
|
||||
"""Get specific nodes and their relationships."""
|
||||
logger.debug(f"Opening nodes entity_ids: {entity_ids}")
|
||||
return await self.repository.find_by_ids(entity_ids)
|
||||
logger.debug(f"Opening nodes path_ids: {path_ids}")
|
||||
return await self.repository.find_by_path_ids(path_ids)
|
||||
|
||||
async def delete_entities(self, entity_ids: List[int]) -> bool:
|
||||
async def delete_entities(self, path_ids: List[str]) -> bool:
|
||||
"""Delete entities and their files."""
|
||||
logger.debug(f"Deleting entities: {entity_ids}")
|
||||
deleted_count = await self.repository.delete_by_ids(entity_ids)
|
||||
logger.debug(f"Deleting entities: {path_ids}")
|
||||
deleted_count = await self.repository.delete_by_path_ids(path_ids)
|
||||
return deleted_count > 0
|
||||
|
||||
@@ -109,7 +109,7 @@ class FileService:
|
||||
async def add_frontmatter(
|
||||
self,
|
||||
*,
|
||||
id: int,
|
||||
id: str,
|
||||
content: str,
|
||||
created: datetime | None = None,
|
||||
updated: datetime | None = None,
|
||||
|
||||
@@ -15,24 +15,29 @@ class EntityOperations(FileOperations):
|
||||
async def create_entity(self, entity: EntitySchema) -> EntityModel:
|
||||
"""Create a new entity and write to filesystem."""
|
||||
logger.debug(f"Creating entity: {entity}")
|
||||
|
||||
db_entity = None
|
||||
file_path = None
|
||||
try:
|
||||
# 1. Create entity in DB
|
||||
db_entity = await self.entity_service.create_entity(entity)
|
||||
|
||||
# 2. Write file and get checksum
|
||||
checksum = await self.write_entity_file(db_entity)
|
||||
file_path, checksum = await self.write_entity_file(db_entity)
|
||||
|
||||
# 3. Update DB with checksum
|
||||
updated = await self.entity_service.update_entity(db_entity.id, {"checksum": checksum})
|
||||
updated = await self.entity_service.update_entity(
|
||||
db_entity.path_id, {"checksum": checksum}
|
||||
)
|
||||
|
||||
return updated
|
||||
|
||||
except Exception as e:
|
||||
# Clean up on any failure
|
||||
if "db_entity" in locals():
|
||||
await self.entity_service.delete_entity(db_entity.id) # pyright: ignore [reportPossiblyUnboundVariable]
|
||||
if "path" in locals():
|
||||
await self.file_service.delete_file(path) # pyright: ignore [reportUndefinedVariable] # noqa: F821
|
||||
if db_entity:
|
||||
await self.entity_service.delete_entity(db_entity.path_id)
|
||||
if file_path:
|
||||
await self.file_service.delete_file(file_path)
|
||||
logger.error(f"Failed to create entity: {e}")
|
||||
raise
|
||||
|
||||
@@ -47,13 +52,13 @@ class EntityOperations(FileOperations):
|
||||
|
||||
return created
|
||||
|
||||
async def delete_entity(self, entity_id: int) -> bool:
|
||||
async def delete_entity(self, path_id: str) -> bool:
|
||||
"""Delete entity and its file."""
|
||||
logger.debug(f"Deleting entity: {entity_id}")
|
||||
logger.debug(f"Deleting entity: {path_id}")
|
||||
|
||||
try:
|
||||
# Get entity first for file deletion
|
||||
entity = await self.entity_service.get_entity(entity_id)
|
||||
entity = await self.entity_service.get_by_path_id(path_id)
|
||||
if not entity:
|
||||
return True # Already deleted
|
||||
|
||||
@@ -62,20 +67,20 @@ class EntityOperations(FileOperations):
|
||||
await self.file_service.delete_file(path)
|
||||
|
||||
# Delete from DB (this will cascade to observations/relations)
|
||||
return await self.entity_service.delete_entity(entity_id)
|
||||
return await self.entity_service.delete_entity(path_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete entity: {e}")
|
||||
raise
|
||||
|
||||
async def delete_entities(self, entity_ids: List[int]) -> bool:
|
||||
async def delete_entities(self, path_ids: List[str]) -> bool:
|
||||
"""Delete multiple entities and their files."""
|
||||
logger.debug(f"Deleting entities: {entity_ids}")
|
||||
logger.debug(f"Deleting entities: {path_ids}")
|
||||
success = True
|
||||
|
||||
# Let errors bubble up
|
||||
for entity_id in entity_ids:
|
||||
await self.delete_entity(entity_id)
|
||||
for path_id in path_ids:
|
||||
await self.delete_entity(path_id)
|
||||
success = True
|
||||
|
||||
return success
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""File operations for knowledge service."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -30,24 +31,24 @@ class FileOperations:
|
||||
"""Generate filesystem path for entity."""
|
||||
return self.base_path / entity.entity_type / f"{entity.name}.md"
|
||||
|
||||
async def write_entity_file(self, entity: EntityModel) -> str:
|
||||
async def write_entity_file(self, entity: EntityModel) -> Tuple[Path, str]:
|
||||
"""Write entity to filesystem and return checksum."""
|
||||
try:
|
||||
# Ensure we have a fresh entity with all relations loaded
|
||||
entity = await self.entity_service.get_entity(entity.id)
|
||||
entity = await self.entity_service.get_by_path_id(entity.path_id)
|
||||
|
||||
# Format content
|
||||
path = self.get_entity_path(entity)
|
||||
entity_content = await self.knowledge_writer.format_content(entity)
|
||||
file_content = await self.file_service.add_frontmatter(
|
||||
id=entity.id,
|
||||
id=entity.path_id,
|
||||
content=entity_content,
|
||||
created=entity.created_at,
|
||||
updated=entity.updated_at,
|
||||
)
|
||||
|
||||
# Write and get checksum
|
||||
return await self.file_service.write_file(path, file_content)
|
||||
return path, await self.file_service.write_file(path, file_content)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write entity file: {e}")
|
||||
|
||||
@@ -18,26 +18,26 @@ class ObservationOperations(RelationOperations):
|
||||
self.observation_service = observation_service
|
||||
|
||||
async def add_observations(
|
||||
self, entity_id: int, observations: List[str], context: str | None = None
|
||||
self, path_id: str, observations: List[str], context: str | None = None
|
||||
) -> EntityModel:
|
||||
"""Add observations to entity and update its file."""
|
||||
logger.debug(f"Adding observations to entity {entity_id}")
|
||||
logger.debug(f"Adding observations to entity {path_id}")
|
||||
|
||||
try:
|
||||
# Get entity to update
|
||||
entity = await self.entity_service.get_entity(entity_id)
|
||||
entity = await self.entity_service.get_by_path_id(path_id)
|
||||
if not entity:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_id}")
|
||||
raise EntityNotFoundError(f"Entity not found: {path_id}")
|
||||
|
||||
# Add observations to DB
|
||||
await self.observation_service.add_observations(entity_id, observations, context)
|
||||
await self.observation_service.add_observations(entity.id, observations, context)
|
||||
|
||||
# Get updated entity
|
||||
updated_entity = await self.entity_service.get_entity(entity_id)
|
||||
updated_entity = await self.entity_service.get_by_path_id(path_id)
|
||||
|
||||
# Write updated file and checksum
|
||||
checksum = await self.write_entity_file(entity)
|
||||
await self.entity_service.update_entity(entity_id, {"checksum": checksum})
|
||||
await self.entity_service.update_entity(path_id, {"checksum": checksum})
|
||||
|
||||
return updated_entity
|
||||
|
||||
@@ -45,25 +45,25 @@ class ObservationOperations(RelationOperations):
|
||||
logger.error(f"Failed to add observations: {e}")
|
||||
raise
|
||||
|
||||
async def delete_observations(self, entity_id: int, observations: List[str]) -> EntityModel:
|
||||
async def delete_observations(self, path_id: str, observations: List[str]) -> EntityModel:
|
||||
"""Delete observations from entity and update its file."""
|
||||
logger.debug(f"Deleting observations from entity {entity_id}")
|
||||
logger.debug(f"Deleting observations from entity {path_id}")
|
||||
|
||||
try:
|
||||
# Get updated entity
|
||||
entity = await self.entity_service.get_entity(entity_id)
|
||||
entity = await self.entity_service.get_by_path_id(path_id)
|
||||
if not entity:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_id}")
|
||||
raise EntityNotFoundError(f"Entity not found: {path_id}")
|
||||
|
||||
# Delete observations from DB
|
||||
await self.observation_service.delete_observations(entity_id, observations)
|
||||
await self.observation_service.delete_observations(entity.id, observations)
|
||||
|
||||
# Write updated file
|
||||
checksum = await self.write_entity_file(entity)
|
||||
await self.entity_service.update_entity(entity_id, {"checksum": checksum})
|
||||
await self.entity_service.update_entity(path_id, {"checksum": checksum})
|
||||
|
||||
# Get final entity with all updates
|
||||
return await self.entity_service.get_entity(entity_id)
|
||||
return await self.entity_service.get_by_path_id(path_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete observations: {e}")
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Sequence, List, Dict, Any
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.models import Relation as RelationModel
|
||||
from basic_memory.schemas import Relation as RelationSchema
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
from basic_memory.services.relation_service import RelationService
|
||||
@@ -21,78 +22,88 @@ class RelationOperations(EntityOperations):
|
||||
async def create_relations(self, relations: List[RelationSchema]) -> Sequence[EntityModel]:
|
||||
"""Create relations and return updated entities."""
|
||||
logger.debug(f"Creating {len(relations)} relations")
|
||||
created_entities = []
|
||||
updated_entity_ids = set()
|
||||
updated_entities = []
|
||||
entities_to_update = set()
|
||||
|
||||
for relation in relations:
|
||||
for rs in relations:
|
||||
try:
|
||||
# Create relation in DB
|
||||
from_entity = await self.entity_service.get_by_path_id(rs.from_id)
|
||||
to_entity = await self.entity_service.get_by_path_id(rs.to_id)
|
||||
|
||||
relation = RelationModel(
|
||||
from_id=from_entity.id,
|
||||
to_id=to_entity.id,
|
||||
relation_type=rs.relation_type,
|
||||
context=rs.context,
|
||||
)
|
||||
# Create rs in DB
|
||||
await self.relation_service.create_relation(relation)
|
||||
|
||||
# Keep track of entities we need to update
|
||||
updated_entity_ids.add(relation.from_id)
|
||||
updated_entity_ids.add(relation.to_id)
|
||||
entities_to_update.add(rs.from_id)
|
||||
entities_to_update.add(rs.to_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create relation: {e}")
|
||||
logger.error(f"Failed to create rs: {e}")
|
||||
continue
|
||||
|
||||
# Get fresh copies of all updated entities
|
||||
for entity_id in updated_entity_ids:
|
||||
for path_id in entities_to_update:
|
||||
try:
|
||||
# Get fresh entity
|
||||
entity = await self.entity_service.get_entity(entity_id)
|
||||
if not entity:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_id}")
|
||||
entity = await self.entity_service.get_by_path_id(path_id)
|
||||
|
||||
# Write updated file
|
||||
checksum = await self.write_entity_file(entity)
|
||||
updated = await self.entity_service.update_entity(entity_id, {"checksum": checksum})
|
||||
_, checksum = await self.write_entity_file(entity)
|
||||
updated = await self.entity_service.update_entity(path_id, {"checksum": checksum})
|
||||
|
||||
created_entities.append(updated)
|
||||
updated_entities.append(updated)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update entity {entity_id}: {e}")
|
||||
logger.error(f"Failed to update entity {path_id}: {e}")
|
||||
continue
|
||||
|
||||
return created_entities
|
||||
|
||||
# select again to eagerly load all relations
|
||||
return await self.entity_service.open_nodes([e.path_id for e in updated_entities])
|
||||
|
||||
async def delete_relations(self, to_delete: List[Dict[str, Any]]) -> Sequence[EntityModel]:
|
||||
"""Delete relations and return all updated entities."""
|
||||
logger.debug(f"Deleting {len(to_delete)} relations")
|
||||
updated_entity_ids = set()
|
||||
updated_entities = []
|
||||
entities_to_update = set()
|
||||
|
||||
try:
|
||||
# Delete relations from DB
|
||||
for relation in to_delete:
|
||||
updated_entity_ids.add(relation["from_id"])
|
||||
updated_entity_ids.add(relation["to_id"])
|
||||
entities_to_update.add(relation["from_id"])
|
||||
entities_to_update.add(relation["to_id"])
|
||||
|
||||
deleted = await self.relation_service.delete_relations(to_delete)
|
||||
if not deleted:
|
||||
logger.warning("No relations were deleted")
|
||||
|
||||
# Get fresh copies of all updated entities
|
||||
updated_entities = []
|
||||
for entity_id in updated_entity_ids:
|
||||
for path_id in entities_to_update:
|
||||
try:
|
||||
# Get fresh entity
|
||||
entity = await self.entity_service.get_entity(entity_id)
|
||||
entity = await self.entity_service.get_by_path_id(path_id)
|
||||
if not entity:
|
||||
raise EntityNotFoundError(f"Entity not found: {entity_id}")
|
||||
raise EntityNotFoundError(f"Entity not found: {path_id}")
|
||||
|
||||
# Write updated file
|
||||
checksum = await self.write_entity_file(entity)
|
||||
updated = await self.entity_service.update_entity(entity_id, {"checksum": checksum})
|
||||
updated = await self.entity_service.update_entity(
|
||||
path_id, {"checksum": checksum}
|
||||
)
|
||||
|
||||
updated_entities.append(updated)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update entity {entity_id}: {e}")
|
||||
logger.error(f"Failed to update entity {path_id}: {e}")
|
||||
continue
|
||||
|
||||
return updated_entities
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete relations: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
@@ -21,7 +21,7 @@ class ObservationService(BaseService[ObservationRepository]):
|
||||
|
||||
async def add_observations(
|
||||
self, entity_id: int, observations: List[str], context: str | None = None
|
||||
) -> List[ObservationModel]:
|
||||
) -> Sequence[ObservationModel]:
|
||||
"""Add multiple observations to an entity."""
|
||||
logger.debug(f"Adding {len(observations)} observations to entity: {entity_id}")
|
||||
return await self.repository.create_all(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Service for managing relations in the database."""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Sequence
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.models import Relation as RelationModel
|
||||
from basic_memory.models import Entity, Relation
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
from basic_memory.schemas import Entity as EntitySchema, Relation
|
||||
from .service import BaseService
|
||||
|
||||
|
||||
@@ -19,13 +18,13 @@ class RelationService(BaseService[RelationRepository]):
|
||||
def __init__(self, relation_repository: RelationRepository):
|
||||
super().__init__(relation_repository)
|
||||
|
||||
async def create_relation(self, relation: Relation) -> RelationModel:
|
||||
async def create_relation(self, relation: Relation) -> Relation:
|
||||
"""Create a new relation in the database."""
|
||||
logger.debug(f"Creating relation: {relation}")
|
||||
return await self.repository.create(relation.model_dump())
|
||||
return await self.repository.add(relation)
|
||||
|
||||
async def delete_relation(
|
||||
self, from_entity: EntitySchema, to_entity: EntitySchema, relation_type: str
|
||||
self, from_entity: Entity, to_entity: Entity, relation_type: str
|
||||
) -> bool:
|
||||
"""Delete a specific relation between entities."""
|
||||
logger.debug(f"Deleting relation between {from_entity.id} and {to_entity.id}")
|
||||
@@ -53,9 +52,8 @@ class RelationService(BaseService[RelationRepository]):
|
||||
|
||||
return deleted
|
||||
|
||||
async def create_relations(self, relations_data: List[Relation]) -> List[RelationModel]:
|
||||
async def create_relations(self, relations: List[Relation]) -> Sequence[Relation]:
|
||||
"""Create multiple relations between entities."""
|
||||
logger.debug(f"Creating {len(relations_data)} relations")
|
||||
return await self.repository.create_all(
|
||||
[Relation.model_dump(relation) for relation in relations_data]
|
||||
)
|
||||
logger.debug(f"Creating {len(relations)} relations")
|
||||
return await self.repository.add_all(relations)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user