fix tests

This commit is contained in:
phernandez
2025-01-07 20:29:59 -06:00
parent 6686c331a5
commit 1c6836d83f
37 changed files with 596 additions and 780 deletions
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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):
+46 -9
View File
@@ -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
+1 -1
View File
@@ -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
+3 -2
View File
@@ -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] = []
+2 -1
View File
@@ -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,
},