mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix tests
This commit is contained in:
@@ -11,8 +11,6 @@ from basic_memory.deps import (
|
||||
from basic_memory.schemas import (
|
||||
CreateEntityRequest,
|
||||
EntityListResponse,
|
||||
SearchNodesRequest,
|
||||
SearchNodesResponse,
|
||||
CreateRelationsRequest,
|
||||
EntityResponse,
|
||||
AddObservationsRequest,
|
||||
@@ -23,7 +21,7 @@ from basic_memory.schemas import (
|
||||
DeleteEntitiesRequest,
|
||||
UpdateEntityRequest,
|
||||
)
|
||||
from basic_memory.schemas.base import PathId, EntityType
|
||||
from basic_memory.schemas.base import PathId
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
|
||||
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
||||
@@ -36,15 +34,15 @@ async def create_entities(
|
||||
data: CreateEntityRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service)
|
||||
search_service=Depends(get_search_service),
|
||||
) -> EntityListResponse:
|
||||
"""Create new entities in the knowledge graph and index them."""
|
||||
entities = await knowledge_service.create_entities(data.entities)
|
||||
|
||||
|
||||
# Index each entity
|
||||
for entity in entities:
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
|
||||
|
||||
return EntityListResponse(
|
||||
entities=[EntityResponse.model_validate(entity) for entity in entities]
|
||||
)
|
||||
@@ -56,21 +54,21 @@ async def update_entity(
|
||||
data: UpdateEntityRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service)
|
||||
search_service=Depends(get_search_service),
|
||||
) -> EntityResponse:
|
||||
"""Update an existing entity and reindex it."""
|
||||
try:
|
||||
# Convert request to dict, excluding None values
|
||||
update_data = data.model_dump(exclude_none=True)
|
||||
|
||||
|
||||
# Update the entity
|
||||
updated_entity = await knowledge_service.update_entity(path_id, **update_data)
|
||||
|
||||
|
||||
# Reindex since content changed
|
||||
await search_service.index_entity(updated_entity, background_tasks=background_tasks)
|
||||
|
||||
|
||||
return EntityResponse.model_validate(updated_entity)
|
||||
|
||||
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Entity with {path_id} not found")
|
||||
|
||||
@@ -80,15 +78,15 @@ async def create_relations(
|
||||
data: CreateRelationsRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service),
|
||||
search_service=Depends(get_search_service),
|
||||
) -> EntityListResponse:
|
||||
"""Create relations between entities and update search index."""
|
||||
updated_entities = await knowledge_service.create_relations(data.relations)
|
||||
|
||||
|
||||
# Reindex updated entities since relations have changed
|
||||
for entity in updated_entities:
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
|
||||
|
||||
return EntityListResponse(
|
||||
entities=[EntityResponse.model_validate(entity) for entity in updated_entities]
|
||||
)
|
||||
@@ -99,17 +97,17 @@ async def add_observations(
|
||||
data: AddObservationsRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service)
|
||||
search_service=Depends(get_search_service),
|
||||
) -> EntityResponse:
|
||||
"""Add observations to an entity and update search index."""
|
||||
logger.debug(f"Adding observations to entity: {data.path_id}")
|
||||
updated_entity = await knowledge_service.add_observations(
|
||||
data.path_id, data.observations, data.context
|
||||
)
|
||||
|
||||
|
||||
# Reindex the entity with new observations
|
||||
await search_service.index_entity(updated_entity, background_tasks=background_tasks)
|
||||
|
||||
|
||||
return EntityResponse.model_validate(updated_entity)
|
||||
|
||||
|
||||
@@ -122,9 +120,9 @@ async def get_entity(path_id: PathId, knowledge_service: KnowledgeServiceDep) ->
|
||||
try:
|
||||
entity = await knowledge_service.get_entity_by_path_id(path_id)
|
||||
entity_response = EntityResponse.model_validate(entity)
|
||||
|
||||
|
||||
# if the entity is a note, we add the content via reading from the file
|
||||
if entity_response.entity_type == EntityType.NOTE:
|
||||
if entity_response.entity_type == "note":
|
||||
content = await knowledge_service.read_entity_content(entity)
|
||||
entity_response.content = content
|
||||
|
||||
@@ -132,8 +130,11 @@ async def get_entity(path_id: PathId, knowledge_service: KnowledgeServiceDep) ->
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Entity with {path_id} not found")
|
||||
|
||||
|
||||
@router.post("/nodes", response_model=EntityListResponse)
|
||||
async def open_nodes(data: OpenNodesRequest, entity_service: EntityServiceDep) -> EntityListResponse:
|
||||
async def open_nodes(
|
||||
data: OpenNodesRequest, entity_service: EntityServiceDep
|
||||
) -> EntityListResponse:
|
||||
"""Open specific nodes by their names."""
|
||||
entities = await entity_service.open_nodes(data.path_ids)
|
||||
return EntityListResponse(
|
||||
@@ -149,15 +150,15 @@ async def delete_entities(
|
||||
data: DeleteEntitiesRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service)
|
||||
search_service=Depends(get_search_service),
|
||||
) -> DeleteEntitiesResponse:
|
||||
"""Delete entities and remove from search index."""
|
||||
deleted = await knowledge_service.delete_entities(data.path_ids)
|
||||
|
||||
|
||||
# Remove each deleted entity from search index
|
||||
for path_id in data.path_ids:
|
||||
background_tasks.add_task(search_service.delete_by_path_id, path_id)
|
||||
|
||||
|
||||
return DeleteEntitiesResponse(deleted=deleted)
|
||||
|
||||
|
||||
@@ -166,15 +167,15 @@ async def delete_observations(
|
||||
data: DeleteObservationsRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service)
|
||||
search_service=Depends(get_search_service),
|
||||
) -> EntityResponse:
|
||||
"""Delete observations and update search index."""
|
||||
path_id = data.path_id
|
||||
updated_entity = await knowledge_service.delete_observations(path_id, data.observations)
|
||||
|
||||
|
||||
# Reindex the entity since observations changed
|
||||
await search_service.index_entity(updated_entity, background_tasks=background_tasks)
|
||||
|
||||
|
||||
return EntityResponse.model_validate(updated_entity)
|
||||
|
||||
|
||||
@@ -183,15 +184,15 @@ async def delete_relations(
|
||||
data: DeleteRelationsRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
knowledge_service: KnowledgeServiceDep,
|
||||
search_service = Depends(get_search_service)
|
||||
search_service=Depends(get_search_service),
|
||||
) -> EntityListResponse:
|
||||
"""Delete relations and update search index."""
|
||||
updated_entities = await knowledge_service.delete_relations(data.relations)
|
||||
|
||||
|
||||
# Reindex entities since relations changed
|
||||
for entity in updated_entities:
|
||||
await search_service.index_entity(entity, background_tasks=background_tasks)
|
||||
|
||||
|
||||
return EntityListResponse(
|
||||
entities=[EntityResponse.model_validate(entity) for entity in updated_entities]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -152,7 +152,7 @@ class KnowledgeParser(MarkdownParser[EntityMarkdown]):
|
||||
relations.append(relation)
|
||||
|
||||
return EntityContent(
|
||||
title=title, description=description, observations=observations, relations=relations
|
||||
title=title, summary=description, observations=observations, relations=relations
|
||||
)
|
||||
|
||||
except ParseError:
|
||||
|
||||
@@ -37,7 +37,7 @@ class EntityContent(BaseModel):
|
||||
"""Content sections of an entity markdown file."""
|
||||
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
summary: Optional[str] = None
|
||||
observations: List[Observation] = []
|
||||
relations: List[Relation] = []
|
||||
context: Optional[str] = None
|
||||
|
||||
@@ -85,7 +85,7 @@ class Entity(Base):
|
||||
return self.incoming_relations + self.outgoing_relations
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Entity(id={self.id}, name='{self.name}', type='{self.entity_type}')"
|
||||
return f"Entity(id={self.id}, name='{self.name}', type='{self.entity_type}', summary='{self.summary}')"
|
||||
|
||||
|
||||
class ObservationCategory(str, Enum):
|
||||
|
||||
@@ -31,13 +31,13 @@ Common Relation Types:
|
||||
- 'extends': Inheritance/extension
|
||||
- 'tested_by': Test coverage
|
||||
"""
|
||||
|
||||
import mimetypes
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Annotated, Dict
|
||||
|
||||
from annotated_types import MinLen, MaxLen
|
||||
from pydantic import BaseModel, BeforeValidator, Field
|
||||
from pydantic import BaseModel, BeforeValidator, Field, model_validator, ValidationError
|
||||
|
||||
|
||||
def to_snake_case(name: str) -> str:
|
||||
@@ -122,15 +122,33 @@ Examples:
|
||||
- "Depends on SQLAlchemy for database operations"
|
||||
"""
|
||||
|
||||
EntityType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
|
||||
"""Classification of entity (e.g., 'person', 'project', 'concept').
|
||||
|
||||
class EntityType(str, Enum):
|
||||
"""Type of entity.
|
||||
The type serves multiple purposes:
|
||||
1. Enables filtering and querying
|
||||
3. Provides context for relations
|
||||
|
||||
- knowledge: Contain information used in the semantic graph
|
||||
- note: Free form information
|
||||
Common types are listed in the module docstring.
|
||||
"""
|
||||
KNOWLEDGE = "knowledge"
|
||||
NOTE = "note"
|
||||
|
||||
ALLOWED_CONTENT_TYPES = {
|
||||
'text/markdown',
|
||||
'text/plain',
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
}
|
||||
|
||||
ContentType = Annotated[
|
||||
str,
|
||||
BeforeValidator(str.lower),
|
||||
Field(pattern=r'^[\w\-\+\.]+/[\w\-\+\.]+$'),
|
||||
Field(json_schema_extra={"examples": list(ALLOWED_CONTENT_TYPES)})
|
||||
]
|
||||
|
||||
|
||||
|
||||
RelationType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
|
||||
"""Type of relationship between entities. Always use active voice present tense.
|
||||
@@ -252,7 +270,11 @@ class Entity(BaseModel):
|
||||
entity_type: EntityType
|
||||
entity_metadata: Optional[Dict] = Field(default=None, description="Optional metadata")
|
||||
content: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
summary: Optional[str] = None
|
||||
content_type: ContentType = Field(
|
||||
description="MIME type of the content (e.g. text/markdown, image/jpeg)",
|
||||
examples=["text/markdown", "image/jpeg"]
|
||||
)
|
||||
observations: List[Observation] = []
|
||||
|
||||
@property
|
||||
@@ -265,3 +287,18 @@ class Entity(BaseModel):
|
||||
def file_path(self):
|
||||
"""Get the file path for this entity based on its path_id."""
|
||||
return f"{self.path_id}.md"
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def infer_content_type(cls, data: Dict) -> Dict:
|
||||
"""Infer content_type from file_path if not provided."""
|
||||
if 'content_type' not in data:
|
||||
# Get path from either file_path or construct from path_id
|
||||
file_path = data.get('file_path') or f"{data.get('name')}.md"
|
||||
|
||||
if not file_path:
|
||||
raise ValidationError("Either file_path or name must be provided")
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
data['content_type'] = mime_type or 'text/plain'
|
||||
|
||||
return data
|
||||
@@ -94,7 +94,7 @@ class UpdateEntityRequest(BaseModel):
|
||||
"""Request to update an existing entity."""
|
||||
name: Optional[str] = None
|
||||
entity_type: Optional[EntityType] = None
|
||||
description: Optional[str] = None
|
||||
summary: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
entity_metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from typing import List, Optional, Dict, Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices
|
||||
|
||||
from basic_memory.schemas.base import Observation, Relation, PathId, Entity, EntityType
|
||||
from basic_memory.schemas.base import Observation, Relation, PathId, Entity, EntityType, ContentType
|
||||
from basic_memory.schemas.request import ObservationCreate
|
||||
|
||||
|
||||
@@ -122,7 +122,8 @@ class EntityResponse(SQLAlchemyModel):
|
||||
name: str
|
||||
entity_type: EntityType
|
||||
entity_metadata: Optional[Dict] = None
|
||||
description: Optional[str] = None
|
||||
content_type: ContentType
|
||||
summary: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
observations: List[ObservationResponse] = []
|
||||
relations: List[RelationResponse] = []
|
||||
|
||||
@@ -18,7 +18,8 @@ def entity_model(entity: EntitySchema):
|
||||
entity_metadata=entity.entity_metadata,
|
||||
path_id=entity.path_id,
|
||||
file_path=entity.file_path,
|
||||
description=entity.description,
|
||||
summary=entity.summary,
|
||||
content_type=entity.content_type,
|
||||
observations=[Observation(content=observation) for observation in entity.observations],
|
||||
)
|
||||
return model
|
||||
|
||||
@@ -10,7 +10,6 @@ from basic_memory.schemas import Entity as EntitySchema
|
||||
from basic_memory.services.entity_service import EntityService
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
from .file_operations import FileOperations
|
||||
from ...models.knowledge import EntityType
|
||||
|
||||
|
||||
class EntityOperations:
|
||||
@@ -41,9 +40,6 @@ class EntityOperations:
|
||||
"""
|
||||
logger.debug(f"Reading entity with path_id: {entity.path_id}")
|
||||
|
||||
|
||||
if entity.entity_type != EntityType.NOTE:
|
||||
raise ValueError(f"Entity type {entity.entity_type} not supported")
|
||||
|
||||
# For notes, read the actual file content
|
||||
file_path = self.file_operations.get_entity_path(entity)
|
||||
|
||||
@@ -8,7 +8,6 @@ from loguru import logger
|
||||
from basic_memory.markdown.knowledge_writer import KnowledgeWriter
|
||||
from basic_memory.markdown.note_writer import NoteWriter
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.services.entity_service import EntityService
|
||||
from basic_memory.services.exceptions import FileOperationError
|
||||
from basic_memory.services.file_service import FileService
|
||||
@@ -54,7 +53,7 @@ class FileOperations:
|
||||
entity = await self.entity_service.get_by_path_id(entity.path_id)
|
||||
|
||||
# Select writer based on entity type
|
||||
writer = self.note_writer if entity.entity_type == EntityType.NOTE else self.knowledge_writer
|
||||
writer = self.note_writer if entity.entity_type == "note" else self.knowledge_writer
|
||||
|
||||
# Get frontmatter and content
|
||||
frontmatter = await writer.format_frontmatter(entity)
|
||||
|
||||
@@ -29,7 +29,8 @@ def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> Enti
|
||||
entity_type=markdown.frontmatter.type,
|
||||
path_id=markdown.frontmatter.id,
|
||||
file_path=file_path,
|
||||
description=markdown.content.description,
|
||||
content_type="text/markdown",
|
||||
summary=markdown.content.summary,
|
||||
observations=[
|
||||
Observation(
|
||||
content=obs.content,
|
||||
@@ -83,7 +84,7 @@ class KnowledgeSyncService:
|
||||
# Update fields from markdown
|
||||
db_entity.name = markdown.content.title
|
||||
db_entity.entity_type = markdown.frontmatter.type
|
||||
db_entity.summary = markdown.content.description
|
||||
db_entity.summary = markdown.content.summary
|
||||
|
||||
# Clear and update observations
|
||||
await self.observation_service.delete_by_entity(db_entity.id)
|
||||
@@ -102,7 +103,7 @@ class KnowledgeSyncService:
|
||||
{
|
||||
"name": db_entity.name,
|
||||
"entity_type": db_entity.entity_type,
|
||||
"description": db_entity.summary,
|
||||
"summary": db_entity.summary,
|
||||
# Mark as incomplete
|
||||
"checksum": None,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user