more validation fixes

This commit is contained in:
phernandez
2024-12-24 21:03:00 -06:00
parent 90ca41fead
commit 061e01893a
6 changed files with 131 additions and 67 deletions
+13 -14
View File
@@ -9,14 +9,13 @@ from basic_memory.deps import (
)
from basic_memory.schemas import (
CreateEntityRequest,
CreateEntityResponse,
EntityListResponse,
SearchNodesRequest,
SearchNodesResponse,
CreateRelationsRequest,
EntityResponse,
AddObservationsRequest,
OpenNodesRequest,
OpenNodesResponse,
DeleteEntitiesResponse,
DeleteObservationsRequest,
DeleteRelationsRequest,
@@ -30,24 +29,24 @@ router = APIRouter(prefix="/knowledge", tags=["knowledge"])
## Create endpoints
@router.post("/entities", response_model=CreateEntityResponse)
@router.post("/entities", response_model=EntityListResponse)
async def create_entities(
data: CreateEntityRequest, knowledge_service: KnowledgeServiceDep
) -> CreateEntityResponse:
) -> EntityListResponse:
"""Create new entities in the knowledge graph."""
entities = await knowledge_service.create_entities(data.entities)
return CreateEntityResponse(
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in entities]
)
@router.post("/relations", response_model=CreateEntityResponse)
@router.post("/relations", response_model=EntityListResponse)
async def create_relations(
data: CreateRelationsRequest, knowledge_service: KnowledgeServiceDep
) -> CreateEntityResponse:
) -> EntityListResponse:
"""Create relations between entities."""
updated_entities = await knowledge_service.create_relations(data.relations)
return CreateEntityResponse(
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in updated_entities]
)
@@ -91,11 +90,11 @@ async def search_nodes(
)
@router.post("/nodes", response_model=OpenNodesResponse)
async def open_nodes(data: OpenNodesRequest, entity_service: EntityServiceDep) -> OpenNodesResponse:
@router.post("/nodes", response_model=EntityListResponse)
async def open_nodes(data: OpenNodesRequest, entity_service: EntityServiceDep) -> EntityListResponse:
"""Open specific nodes by their names."""
entities = await entity_service.open_nodes(data.entity_ids)
return OpenNodesResponse(
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in entities]
)
@@ -122,12 +121,12 @@ async def delete_observations(
return EntityResponse.model_validate(updated_entity)
@router.post("/relations/delete", response_model=CreateEntityResponse)
@router.post("/relations/delete", response_model=EntityListResponse)
async def delete_relations(
data: DeleteRelationsRequest, knowledge_service: KnowledgeServiceDep
) -> CreateEntityResponse:
) -> EntityListResponse:
"""Delete relations between entities."""
updated_entities = await knowledge_service.delete_relations(data.relations)
return CreateEntityResponse(
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in updated_entities]
)
+2 -7
View File
@@ -36,9 +36,8 @@ from basic_memory.schemas.response import (
ObservationResponse,
RelationResponse,
EntityResponse,
CreateEntityResponse,
EntityListResponse,
SearchNodesResponse,
OpenNodesResponse,
DeleteEntitiesResponse,
)
@@ -56,17 +55,13 @@ __all__ = [
"SearchNodesRequest",
"OpenNodesRequest",
"CreateRelationsRequest",
"DocumentCreateRequest",
"DocumentUpdateRequest",
# Responses
"SQLAlchemyModel",
"ObservationResponse",
"ObservationsResponse",
"RelationResponse",
"EntityResponse",
"CreateEntityResponse",
"EntityListResponse",
"SearchNodesResponse",
"OpenNodesResponse",
"DeleteEntitiesResponse",
# Delete Operations
"DeleteEntitiesRequest",
+20 -3
View File
@@ -48,8 +48,10 @@ def to_snake_case(name: str) -> str:
memory-service -> memory_service
Memory_Service -> memory_service
"""
# Replace spaces and hyphens with underscores
s1 = re.sub(r"[\s\-]", "_", name)
name = name.strip()
# Replace spaces and hyphens and . with underscores
s1 = re.sub(r"[\s\-\\.]", "_", name)
# Insert underscore between camelCase
s2 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1)
@@ -58,7 +60,22 @@ def to_snake_case(name: str) -> str:
return s2.lower()
PathId = Annotated[str, BeforeValidator(to_snake_case)]
def validate_path_format(path: str) -> str:
"""Validate path has the correct format: type/name."""
if not path or not isinstance(path, str):
raise ValueError("Path must be a non-empty string")
parts = path.split('/')
if len(parts) != 2:
raise ValueError("Path must be in format: type/name")
type_part, name_part = parts
if not type_part or not name_part:
raise ValueError("Both type and name must be non-empty")
return path
PathId = Annotated[str, BeforeValidator(to_snake_case), BeforeValidator(validate_path_format)]
"""Unique identifier in format '{path}/{normalized_name}'."""
Observation = Annotated[str, MinLen(1), MaxLen(1000)]
+6 -31
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
from basic_memory.schemas.base import Observation, Relation, PathId, Entity, EntityType
class SQLAlchemyModel(BaseModel):
@@ -38,7 +38,6 @@ class ObservationResponse(SQLAlchemyModel):
Example Response:
{
"id": 123,
"content": "Implements SQLite storage for persistence"
}
"""
@@ -112,19 +111,20 @@ class EntityResponse(SQLAlchemyModel):
}
"""
# Note this Class does not inherit form Entity because of the Entity.path_id semantics
path_id: PathId
name: str
entity_type: str
entity_type: EntityType
description: Optional[str] = None
observations: List[ObservationResponse] = []
relations: List[RelationResponse] = []
class CreateEntityResponse(SQLAlchemyModel):
class EntityListResponse(SQLAlchemyModel):
"""Response for create_entities operation.
Returns complete information about all created entities,
including their generated IDs, initial observations,
Returns complete information about entities returned from the service,
including their path_ids, observations,
and any established relations.
Example Response:
@@ -137,7 +137,6 @@ class CreateEntityResponse(SQLAlchemyModel):
"description": "Knowledge graph search",
"observations": [
{
"id": 125,
"content": "Implements full-text search"
}
],
@@ -150,7 +149,6 @@ class CreateEntityResponse(SQLAlchemyModel):
"description": "API Reference",
"observations": [
{
"id": 126,
"content": "Documents REST endpoints"
}
],
@@ -192,29 +190,6 @@ class SearchNodesResponse(SQLAlchemyModel):
query: str
class OpenNodesResponse(SQLAlchemyModel):
"""Response for retrieving specific entities.
Returns complete Entity objects for all found entities.
Entities that don't exist are silently skipped.
Example Response:
{
"entities": [
{
"path_id": "component/memory_service",
"name": "MemoryService",
"entity_type": "component",
"description": "Core service",
"observations": [...],
"relations": [...]
}
]
}
"""
entities: List[EntityResponse]
class DeleteEntitiesResponse(SQLAlchemyModel):
"""Response indicating successful entity deletion.