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,
|
||||
},
|
||||
|
||||
@@ -4,11 +4,10 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
|
||||
from basic_memory.models.knowledge import Entity, Observation, EntityType
|
||||
from basic_memory.models.knowledge import Entity, Observation
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedEntityList
|
||||
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@@ -18,50 +17,54 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
|
||||
entities = [
|
||||
Entity(
|
||||
name="Memory Service",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Core memory service",
|
||||
entity_type="test",
|
||||
content_type="text/markdown",
|
||||
summary="Core memory service",
|
||||
path_id="component/memory_service",
|
||||
file_path="component/memory_service.md",
|
||||
observations=[
|
||||
Observation(category="tech", content="Using SQLite for storage"),
|
||||
Observation(category="design", content="Local-first architecture"),
|
||||
]
|
||||
],
|
||||
),
|
||||
Entity(
|
||||
name="File Format",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="File format spec",
|
||||
entity_type="test",
|
||||
content_type="text/markdown",
|
||||
summary="File format spec",
|
||||
path_id="spec/file_format",
|
||||
file_path="spec/file_format.md",
|
||||
observations=[
|
||||
Observation(category="feature", content="Support for frontmatter"),
|
||||
Observation(category="tech", content="UTF-8 encoding"),
|
||||
]
|
||||
],
|
||||
),
|
||||
Entity(
|
||||
name="Technical Decision",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Architecture decision",
|
||||
entity_type="test",
|
||||
content_type="text/markdown",
|
||||
summary="Architecture decision",
|
||||
path_id="decision/tech_choice",
|
||||
file_path="decision/tech_choice.md",
|
||||
observations=[
|
||||
Observation(category="note", content="Team discussed options"),
|
||||
Observation(category="design", content="Selected for scalability"),
|
||||
]
|
||||
],
|
||||
),
|
||||
# Add another technical component for sorting tests
|
||||
Entity(
|
||||
name="API Service",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="API layer",
|
||||
entity_type="test",
|
||||
content_type="text/markdown",
|
||||
summary="API layer",
|
||||
path_id="component/api_service",
|
||||
file_path="component/api_service.md",
|
||||
observations=[
|
||||
Observation(category="tech", content="FastAPI based"),
|
||||
]
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
created = await entity_repository.add_all(entities)
|
||||
return created
|
||||
|
||||
@@ -71,18 +74,18 @@ async def test_get_entity_types(client: AsyncClient, test_entities):
|
||||
# Get types
|
||||
response = await client.get("/discovery/entity-types")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# Parse response
|
||||
data = EntityTypeList.model_validate(response.json())
|
||||
|
||||
|
||||
# Should have types from test data
|
||||
assert len(data.types) > 0
|
||||
assert "knowledge" in data.types
|
||||
|
||||
assert "test" in data.types
|
||||
|
||||
# Types should all be strings
|
||||
assert isinstance(data.types, list)
|
||||
assert all(isinstance(t, str) for t in data.types)
|
||||
|
||||
|
||||
# Types should be unique
|
||||
assert len(data.types) == len(set(data.types))
|
||||
|
||||
@@ -92,21 +95,21 @@ async def test_get_observation_categories(client: AsyncClient, test_entities):
|
||||
# Get categories
|
||||
response = await client.get("/discovery/observation-categories")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# Parse response
|
||||
data = ObservationCategoryList.model_validate(response.json())
|
||||
|
||||
|
||||
# Should have categories from test data
|
||||
assert len(data.categories) > 0
|
||||
assert "tech" in data.categories
|
||||
assert "design" in data.categories
|
||||
assert "feature" in data.categories
|
||||
assert "note" in data.categories
|
||||
|
||||
|
||||
# Categories should all be strings
|
||||
assert isinstance(data.categories, list)
|
||||
assert all(isinstance(c, str) for c in data.categories)
|
||||
|
||||
|
||||
# Categories should be unique
|
||||
assert len(data.categories) == len(set(data.categories))
|
||||
|
||||
@@ -114,17 +117,17 @@ async def test_get_observation_categories(client: AsyncClient, test_entities):
|
||||
async def test_list_entities_by_type(client: AsyncClient, test_entities):
|
||||
"""Test listing entities by type."""
|
||||
# List technical components
|
||||
response = await client.get("/discovery/entities/knowledge")
|
||||
response = await client.get("/discovery/entities/test")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# Parse response
|
||||
data = TypedEntityList.model_validate(response.json())
|
||||
|
||||
|
||||
# Check response structure
|
||||
assert data.entity_type == "knowledge"
|
||||
assert data.entity_type == "test"
|
||||
assert len(data.entities) == 4
|
||||
assert data.total == 4
|
||||
|
||||
|
||||
# Verify content
|
||||
names = {e.name for e in data.entities}
|
||||
assert "Memory Service" in names
|
||||
@@ -152,7 +155,7 @@ async def test_list_entities_empty_type(client: AsyncClient, test_entities):
|
||||
"""Test listing entities for a type that doesn't exist."""
|
||||
response = await client.get("/discovery/entities/nonexistent_type")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
data = TypedEntityList.model_validate(response.json())
|
||||
assert data.entity_type == "nonexistent_type"
|
||||
assert len(data.entities) == 0
|
||||
|
||||
@@ -10,7 +10,7 @@ from basic_memory.schemas import (
|
||||
EntityResponse,
|
||||
EntityListResponse,
|
||||
ObservationResponse,
|
||||
RelationResponse, EntityType,
|
||||
RelationResponse,
|
||||
)
|
||||
from basic_memory.schemas.search import SearchItemType, SearchResponse
|
||||
|
||||
@@ -18,7 +18,7 @@ from basic_memory.schemas.search import SearchItemType, SearchResponse
|
||||
async def create_entity(client) -> EntityResponse:
|
||||
data = {
|
||||
"name": "TestEntity",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"observations": ["First observation", "Second observation"],
|
||||
}
|
||||
# Create an entity
|
||||
@@ -63,8 +63,8 @@ async def add_observations(client, path_id: str) -> List[ObservationResponse]:
|
||||
async def create_related_entities(client) -> List[RelationResponse]: # pyright: ignore [reportReturnType]
|
||||
# Create two entities to relate
|
||||
entities = [
|
||||
{"name": "SourceEntity", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "TargetEntity", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "SourceEntity", "entity_type": "test"},
|
||||
{"name": "TargetEntity", "entity_type": "test"},
|
||||
]
|
||||
create_response = await client.post("/knowledge/entities", json={"entities": entities})
|
||||
created = create_response.json()["entities"]
|
||||
@@ -114,7 +114,7 @@ async def test_create_entities(client: AsyncClient):
|
||||
async def test_get_entity(client: AsyncClient):
|
||||
"""Should retrieve an entity by path ID."""
|
||||
# First create an entity
|
||||
data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
|
||||
data = {"name": "TestEntity", "entity_type": "test"}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [data]})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
@@ -127,7 +127,7 @@ async def test_get_entity(client: AsyncClient):
|
||||
assert response.status_code == 200
|
||||
entity = response.json()
|
||||
assert entity["name"] == "TestEntity"
|
||||
assert entity["entity_type"] == EntityType.KNOWLEDGE
|
||||
assert entity["entity_type"] == "test"
|
||||
assert entity["path_id"] == "test_entity"
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ async def test_create_relations(client: AsyncClient):
|
||||
async def test_add_observations(client: AsyncClient):
|
||||
"""Should add observations to an entity."""
|
||||
# Create an entity first
|
||||
data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
|
||||
data = {"name": "TestEntity", "entity_type": "test"}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [data]})
|
||||
|
||||
path_id = "test_entity"
|
||||
@@ -154,14 +154,13 @@ async def test_add_observations(client: AsyncClient):
|
||||
assert len(entity["observations"]) == 2
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_nodes(client: AsyncClient):
|
||||
"""Should open multiple nodes by path IDs."""
|
||||
# Create a few entities with different names
|
||||
entities = [
|
||||
{"name": "AlphaTest", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "BetaTest", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "AlphaTest", "entity_type": "test"},
|
||||
{"name": "BetaTest", "entity_type": "test"},
|
||||
]
|
||||
await client.post("/knowledge/entities", json={"entities": entities})
|
||||
|
||||
@@ -177,7 +176,7 @@ async def test_open_nodes(client: AsyncClient):
|
||||
assert len(data["entities"]) == 1
|
||||
entity = data["entities"][0]
|
||||
assert entity["name"] == "AlphaTest"
|
||||
assert entity["entity_type"] == EntityType.KNOWLEDGE
|
||||
assert entity["entity_type"] == "test"
|
||||
assert entity["path_id"] == "alpha_test"
|
||||
|
||||
|
||||
@@ -185,7 +184,7 @@ async def test_open_nodes(client: AsyncClient):
|
||||
async def test_delete_entity(client: AsyncClient):
|
||||
"""Test DELETE /knowledge/entities with path ID."""
|
||||
# Create test entity
|
||||
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
|
||||
entity_data = {"name": "TestEntity", "entity_type": "test"}
|
||||
await client.post("/knowledge/entities", json={"entities": [entity_data]})
|
||||
|
||||
# Test deletion
|
||||
@@ -206,8 +205,8 @@ async def test_delete_entity_bulk(client: AsyncClient):
|
||||
"""Test bulk entity deletion using path IDs."""
|
||||
# Create test entities
|
||||
entities = [
|
||||
{"name": "Entity1", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "Entity2", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "Entity1", "entity_type": "test"},
|
||||
{"name": "Entity2", "entity_type": "test"},
|
||||
]
|
||||
await client.post("/knowledge/entities", json={"entities": entities})
|
||||
|
||||
@@ -229,14 +228,12 @@ async def test_delete_entity_bulk(client: AsyncClient):
|
||||
async def test_delete_entity_with_observations(client, observation_repository):
|
||||
"""Test cascading delete with observations."""
|
||||
# Create test entity and add observations
|
||||
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
|
||||
entity_data = {"name": "TestEntity", "entity_type": "test"}
|
||||
await client.post("/knowledge/entities", json={"entities": [entity_data]})
|
||||
await add_observations(client, "TestEntity")
|
||||
|
||||
# Delete entity
|
||||
response = await client.post(
|
||||
"/knowledge/entities/delete", json={"path_ids": ["TestEntity"]}
|
||||
)
|
||||
response = await client.post("/knowledge/entities/delete", json={"path_ids": ["TestEntity"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"deleted": True}
|
||||
|
||||
@@ -249,7 +246,7 @@ async def test_delete_entity_with_observations(client, observation_repository):
|
||||
async def test_delete_observations(client, observation_repository):
|
||||
"""Test deleting specific observations."""
|
||||
# Create entity and add observations
|
||||
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
|
||||
entity_data = {"name": "TestEntity", "entity_type": "test"}
|
||||
await client.post("/knowledge/entities", json={"entities": [entity_data]})
|
||||
observations = await add_observations(client, "TestEntity") # adds 2
|
||||
|
||||
@@ -293,9 +290,7 @@ async def test_delete_relations(client, relation_repository):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_entity(client: AsyncClient):
|
||||
"""Test deleting a nonexistent entity by path ID."""
|
||||
response = await client.post(
|
||||
"/knowledge/entities/delete", json={"path_ids": ["non_existent"]}
|
||||
)
|
||||
response = await client.post("/knowledge/entities/delete", json={"path_ids": ["non_existent"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"deleted": True}
|
||||
|
||||
@@ -304,7 +299,7 @@ async def test_delete_nonexistent_entity(client: AsyncClient):
|
||||
async def test_delete_nonexistent_observations(client: AsyncClient):
|
||||
"""Test deleting nonexistent observations."""
|
||||
# Create test entity
|
||||
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
|
||||
entity_data = {"name": "TestEntity", "entity_type": "test"}
|
||||
await client.post("/knowledge/entities", json={"entities": [entity_data]})
|
||||
|
||||
request_data = {"path_id": "TestEntity", "observations": ["Nonexistent observation"]}
|
||||
@@ -336,21 +331,20 @@ async def test_delete_nonexistent_relations(client: AsyncClient):
|
||||
assert del_response.entities == []
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_knowledge_flow(client: AsyncClient):
|
||||
"""Test complete knowledge graph flow with path IDs."""
|
||||
# 1. Create main entities
|
||||
main_entities = [
|
||||
{"name": "MainEntity", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "NonEntity", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "MainEntity", "entity_type": "test"},
|
||||
{"name": "NonEntity", "entity_type": "test"},
|
||||
]
|
||||
await client.post("/knowledge/entities", json={"entities": main_entities})
|
||||
|
||||
# 2. Create related entities
|
||||
related_entities = [
|
||||
{"name": "RelatedOne", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "RelatedTwo", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "RelatedOne", "entity_type": "test"},
|
||||
{"name": "RelatedTwo", "entity_type": "test"},
|
||||
]
|
||||
await client.post("/knowledge/entities", json={"entities": related_entities})
|
||||
|
||||
@@ -374,7 +368,7 @@ async def test_full_knowledge_flow(client: AsyncClient):
|
||||
)
|
||||
assert relations_response.status_code == 200
|
||||
relations_entities = relations_response.json()
|
||||
assert len(relations_entities["entities"]) == 3
|
||||
assert len(relations_entities["entities"]) == 3
|
||||
|
||||
# 4. Add observations to main entity
|
||||
await client.post(
|
||||
@@ -402,9 +396,7 @@ async def test_full_knowledge_flow(client: AsyncClient):
|
||||
# 6. Search should find all related entities
|
||||
search = await client.post("/search/", json={"text": "Related"})
|
||||
matches = search.json()["results"]
|
||||
assert (
|
||||
len(matches) == 1
|
||||
)
|
||||
assert len(matches) == 1
|
||||
|
||||
# 7. Delete main entity
|
||||
response = await client.post(
|
||||
@@ -424,7 +416,7 @@ async def test_entity_indexing(client: AsyncClient):
|
||||
"""Test entity creation includes search indexing."""
|
||||
data = {
|
||||
"name": "SearchTest",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"observations": ["Unique searchable observation"],
|
||||
}
|
||||
|
||||
@@ -449,7 +441,7 @@ async def test_observation_update_indexing(client: AsyncClient):
|
||||
# Create entity
|
||||
data = {
|
||||
"name": "TestEntity",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"observations": ["Initial observation"],
|
||||
}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [data]})
|
||||
@@ -479,7 +471,7 @@ async def test_entity_delete_indexing(client: AsyncClient):
|
||||
"""Test deleted entities are removed from search index."""
|
||||
data = {
|
||||
"name": "DeleteTest",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"observations": ["Searchable observation that should be removed"],
|
||||
}
|
||||
|
||||
@@ -514,8 +506,8 @@ async def test_relation_indexing(client: AsyncClient):
|
||||
"""Test relations are included in search index."""
|
||||
# Create entities
|
||||
entities = [
|
||||
{"name": "SourceTest", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "TargetTest", "entity_type": EntityType.KNOWLEDGE},
|
||||
{"name": "SourceTest", "entity_type": "test"},
|
||||
{"name": "TargetTest", "entity_type": "test"},
|
||||
]
|
||||
create_response = await client.post("/knowledge/entities", json={"entities": entities})
|
||||
assert create_response.status_code == 200
|
||||
@@ -541,7 +533,7 @@ async def test_relation_indexing(client: AsyncClient):
|
||||
"/search/", json={"text": "sphinx relation", "types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
search_result = SearchResponse.model_validate(search_response.json())
|
||||
assert len(search_result.results) == 2 # Both source and target entities
|
||||
assert len(search_result.results) == 2 # Both source and target entities
|
||||
path_ids = {r.path_id for r in search_result.results}
|
||||
assert path_ids == {"source_test", "target_test"}
|
||||
|
||||
@@ -552,9 +544,9 @@ async def test_update_entity_basic(client: AsyncClient):
|
||||
# Create initial entity
|
||||
data = {
|
||||
"name": "test",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"description": "Initial description",
|
||||
"entity_metadata": {"status": "draft"}
|
||||
"entity_type": "test",
|
||||
"summary": "Initial description",
|
||||
"entity_metadata": {"status": "draft"},
|
||||
}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [data]})
|
||||
entity = response.json()["entities"][0]
|
||||
@@ -562,7 +554,7 @@ async def test_update_entity_basic(client: AsyncClient):
|
||||
# Update basic fields
|
||||
update_data = {
|
||||
"name": "updated-test",
|
||||
"description": "Updated description",
|
||||
"summary": "Updated description",
|
||||
}
|
||||
response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data)
|
||||
assert response.status_code == 200
|
||||
@@ -570,7 +562,7 @@ async def test_update_entity_basic(client: AsyncClient):
|
||||
|
||||
# Verify updates
|
||||
assert updated["name"] == "updated-test"
|
||||
assert updated["description"] == "Updated description"
|
||||
assert updated["summary"] == "Updated description"
|
||||
assert updated["entity_metadata"]["status"] == "draft" # Preserved
|
||||
|
||||
|
||||
@@ -578,19 +570,14 @@ async def test_update_entity_basic(client: AsyncClient):
|
||||
async def test_update_entity_content(client: AsyncClient):
|
||||
"""Test updating content for different entity types."""
|
||||
# Create a note entity
|
||||
note_data = {
|
||||
"name": "test-note",
|
||||
"entity_type": EntityType.NOTE,
|
||||
"description": "Test note"
|
||||
}
|
||||
note_data = {"name": "test-note", "entity_type": "note", "summary": "Test note"}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [note_data]})
|
||||
note = response.json()["entities"][0]
|
||||
|
||||
# Update note content
|
||||
new_content = "# Updated Note\n\nNew content."
|
||||
response = await client.put(
|
||||
f"/knowledge/entities/{note['path_id']}",
|
||||
json={"content": new_content}
|
||||
f"/knowledge/entities/{note['path_id']}", json={"content": new_content}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
updated = response.json()
|
||||
@@ -608,49 +595,39 @@ async def test_update_entity_type_conversion(client: AsyncClient):
|
||||
# Create a note
|
||||
note_data = {
|
||||
"name": "test-note",
|
||||
"entity_type": EntityType.NOTE,
|
||||
"description": "Test note",
|
||||
"content": "# Test Note\n\nInitial content."
|
||||
"entity_type": "note",
|
||||
"summary": "Test note",
|
||||
"content": "# Test Note\n\nInitial content.",
|
||||
}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [note_data]})
|
||||
note = response.json()["entities"][0]
|
||||
|
||||
# Convert to knowledge type
|
||||
response = await client.put(
|
||||
f"/knowledge/entities/{note['path_id']}",
|
||||
json={"entity_type": EntityType.KNOWLEDGE}
|
||||
f"/knowledge/entities/{note['path_id']}", json={"entity_type": "test"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
updated = response.json()
|
||||
|
||||
# Verify conversion
|
||||
assert updated["entity_type"] == EntityType.KNOWLEDGE
|
||||
assert updated["entity_type"] == "test"
|
||||
|
||||
# Get latest to verify file format
|
||||
response = await client.get(f"/knowledge/entities/{updated['path_id']}")
|
||||
knowledge = response.json()
|
||||
assert knowledge.get("content") is None
|
||||
assert knowledge.get("content") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_entity_metadata(client: AsyncClient):
|
||||
"""Test updating entity metadata."""
|
||||
# Create entity
|
||||
data = {
|
||||
"name": "test",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_metadata": {"status": "draft"}
|
||||
}
|
||||
data = {"name": "test", "entity_type": "test", "entity_metadata": {"status": "draft"}}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [data]})
|
||||
entity = response.json()["entities"][0]
|
||||
|
||||
# Update metadata
|
||||
update_data = {
|
||||
"entity_metadata": {
|
||||
"status": "final",
|
||||
"reviewed": True
|
||||
}
|
||||
}
|
||||
update_data = {"entity_metadata": {"status": "final", "reviewed": True}}
|
||||
response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data)
|
||||
assert response.status_code == 200
|
||||
updated = response.json()
|
||||
@@ -663,10 +640,7 @@ async def test_update_entity_metadata(client: AsyncClient):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_entity_not_found(client: AsyncClient):
|
||||
"""Test updating non-existent entity."""
|
||||
response = await client.put(
|
||||
"/knowledge/entities/nonexistent",
|
||||
json={"name": "new-name"}
|
||||
)
|
||||
response = await client.put("/knowledge/entities/nonexistent", json={"name": "new-name"})
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@@ -674,59 +648,62 @@ async def test_update_entity_not_found(client: AsyncClient):
|
||||
async def test_update_entity_search_index(client: AsyncClient):
|
||||
"""Test search index is updated after entity changes."""
|
||||
# Create entity
|
||||
data = {
|
||||
"name": "test",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"description": "Initial searchable content"
|
||||
}
|
||||
data = {"name": "test", "entity_type": "test", "summary": "Initial searchable content"}
|
||||
response = await client.post("/knowledge/entities", json={"entities": [data]})
|
||||
entity = response.json()["entities"][0]
|
||||
|
||||
# Update with new searchable content
|
||||
update_data = {
|
||||
"description": "Updated with unique sphinx marker"
|
||||
}
|
||||
update_data = {"summary": "Updated with unique sphinx marker"}
|
||||
response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Search should find new content
|
||||
search_response = await client.post(
|
||||
"/search/",
|
||||
json={"text": "sphinx marker", "types": [SearchItemType.ENTITY.value]}
|
||||
"/search/", json={"text": "sphinx marker", "types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
results = search_response.json()["results"]
|
||||
assert len(results) == 1
|
||||
assert results[0]["path_id"] == entity["path_id"]
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_entity_with_relations(client: AsyncClient):
|
||||
"""Test get response includes relations for both types."""
|
||||
# Create a note and knowledge entity
|
||||
note = await client.post("/knowledge/entities", json={"entities": [{
|
||||
"name": "test-note",
|
||||
"entity_type": EntityType.NOTE,
|
||||
"description": "Test note"
|
||||
}]})
|
||||
knowledge = await client.post("/knowledge/entities", json={"entities": [{
|
||||
"name": "test-knowledge",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"description": "Test knowledge"
|
||||
}]})
|
||||
note = await client.post(
|
||||
"/knowledge/entities",
|
||||
json={
|
||||
"entities": [{"name": "test-note", "entity_type": "note", "summary": "Test note"}]
|
||||
},
|
||||
)
|
||||
knowledge = await client.post(
|
||||
"/knowledge/entities",
|
||||
json={
|
||||
"entities": [
|
||||
{"name": "test-knowledge", "entity_type": "test", "summary": "Test knowledge"}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Add some relations between them
|
||||
await client.post("/knowledge/relations", json={
|
||||
"relations": [{
|
||||
"from_id": note.json()["entities"][0]["path_id"],
|
||||
"to_id": knowledge.json()["entities"][0]["path_id"],
|
||||
"relation_type": "references"
|
||||
}]
|
||||
})
|
||||
await client.post(
|
||||
"/knowledge/relations",
|
||||
json={
|
||||
"relations": [
|
||||
{
|
||||
"from_id": note.json()["entities"][0]["path_id"],
|
||||
"to_id": knowledge.json()["entities"][0]["path_id"],
|
||||
"relation_type": "references",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Verify GET returns relations for both types
|
||||
note_response = await client.get(f"/knowledge/entities/{note.json()['entities'][0]['path_id']}")
|
||||
knowledge_response = await client.get(f"/knowledge/entities/{knowledge.json()['entities'][0]['path_id']}")
|
||||
knowledge_response = await client.get(
|
||||
f"/knowledge/entities/{knowledge.json()['entities'][0]['path_id']}"
|
||||
)
|
||||
|
||||
assert len(note_response.json()["relations"]) == 1
|
||||
assert len(knowledge_response.json()["relations"]) == 1
|
||||
|
||||
@@ -5,28 +5,30 @@ from datetime import datetime, timezone
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import text
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.schemas import EntityType
|
||||
from basic_memory.schemas.search import SearchQuery, SearchItemType, SearchResponse
|
||||
from basic_memory.schemas.search import SearchItemType, SearchResponse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_entity():
|
||||
"""Create a test entity."""
|
||||
|
||||
class Entity:
|
||||
id = 1
|
||||
name = "TestComponent"
|
||||
entity_type = EntityType.KNOWLEDGE
|
||||
entity_metadata = { "test": "test"}
|
||||
entity_type = "test"
|
||||
entity_metadata = {"test": "test"}
|
||||
path_id = "component/test_component"
|
||||
file_path = "entities/component/test_component.md"
|
||||
description = "A test component for search testing"
|
||||
summary = "A test component for search testing"
|
||||
content_type = "text/markdown"
|
||||
created_at = datetime.now(timezone.utc)
|
||||
updated_at = datetime.now(timezone.utc)
|
||||
observations = []
|
||||
relations = []
|
||||
return Entity()
|
||||
|
||||
return Entity()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
@@ -36,16 +38,10 @@ async def indexed_entity(init_search_index, test_entity, search_service):
|
||||
return test_entity
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_basic(client, indexed_entity):
|
||||
"""Test basic text search."""
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test component"
|
||||
}
|
||||
)
|
||||
response = await client.post("/search/", json={"text": "test component"})
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 1
|
||||
@@ -57,23 +53,15 @@ async def test_search_with_type_filter(client, indexed_entity):
|
||||
"""Test search with type filter."""
|
||||
# Should find with correct type
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"types": [SearchItemType.ENTITY.value]
|
||||
}
|
||||
"/search/", json={"text": "test", "types": [SearchItemType.ENTITY.value]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 1
|
||||
|
||||
|
||||
# Should not find with wrong type
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"types": [SearchItemType.DOCUMENT.value]
|
||||
}
|
||||
"/search/", json={"text": "test", "types": [SearchItemType.DOCUMENT.value]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
@@ -84,25 +72,13 @@ async def test_search_with_type_filter(client, indexed_entity):
|
||||
async def test_search_with_entity_type_filter(client, indexed_entity):
|
||||
"""Test search with entity type filter."""
|
||||
# Should find with correct entity type
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"entity_types": [EntityType.KNOWLEDGE]
|
||||
}
|
||||
)
|
||||
response = await client.post("/search/", json={"text": "test", "entity_types": ["test"]})
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 1
|
||||
|
||||
|
||||
# Should not find with wrong entity type
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"entity_types": [EntityType.NOTE]
|
||||
}
|
||||
)
|
||||
response = await client.post("/search/", json={"text": "test", "entity_types": ["note"]})
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 0
|
||||
@@ -114,24 +90,16 @@ async def test_search_with_date_filter(client, indexed_entity):
|
||||
# Should find with past date
|
||||
past_date = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"after_date": past_date.isoformat()
|
||||
}
|
||||
"/search/", json={"text": "test", "after_date": past_date.isoformat()}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 1
|
||||
|
||||
|
||||
# Should not find with future date
|
||||
future_date = datetime(2030, 1, 1, tzinfo=timezone.utc)
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={
|
||||
"text": "test",
|
||||
"after_date": future_date.isoformat()
|
||||
}
|
||||
"/search/", json={"text": "test", "after_date": future_date.isoformat()}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
@@ -142,49 +110,34 @@ async def test_search_with_date_filter(client, indexed_entity):
|
||||
async def test_search_scoring(client, indexed_entity):
|
||||
"""Test search result scoring."""
|
||||
# Exact match should score higher
|
||||
exact_response = await client.post(
|
||||
"/search/",
|
||||
json={"text": "TestComponent"}
|
||||
)
|
||||
|
||||
exact_response = await client.post("/search/", json={"text": "TestComponent"})
|
||||
|
||||
# Partial match should score lower
|
||||
partial_response = await client.post(
|
||||
"/search/",
|
||||
json={"text": "test"}
|
||||
)
|
||||
|
||||
partial_response = await client.post("/search/", json={"text": "test"})
|
||||
|
||||
assert exact_response.status_code == 200
|
||||
assert partial_response.status_code == 200
|
||||
|
||||
exact_result = SearchResponse.model_validate(exact_response.json())
|
||||
partial_result = SearchResponse.model_validate(partial_response.json())
|
||||
|
||||
|
||||
exact_score = exact_result.results[0].score
|
||||
partial_score = partial_result.results[0].score
|
||||
|
||||
|
||||
assert exact_score > partial_score
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_empty(search_service, client):
|
||||
"""Test search with no matches."""
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={"text": "nonexistent"}
|
||||
)
|
||||
response = await client.post("/search/", json={"text": "nonexistent"})
|
||||
assert response.status_code == 200
|
||||
search_result = SearchResponse.model_validate(response.json())
|
||||
assert len(search_result.results) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reindex(
|
||||
client,
|
||||
search_service,
|
||||
entity_service,
|
||||
test_entity,
|
||||
session_maker
|
||||
):
|
||||
async def test_reindex(client, search_service, entity_service, test_entity, session_maker):
|
||||
"""Test reindex endpoint."""
|
||||
# Create test entity and document
|
||||
await entity_service.create_entity(test_entity)
|
||||
@@ -195,10 +148,7 @@ async def test_reindex(
|
||||
await session.commit()
|
||||
|
||||
# Verify nothing is searchable
|
||||
response = await client.post(
|
||||
"/search/",
|
||||
json={"text": "test"}
|
||||
)
|
||||
response = await client.post("/search/", json={"text": "test"})
|
||||
search_results = SearchResponse.model_validate(response.json())
|
||||
assert len(search_results.results) == 0
|
||||
|
||||
@@ -208,12 +158,9 @@ async def test_reindex(
|
||||
assert reindex_response.json()["status"] == "ok"
|
||||
|
||||
# Verify content is searchable again
|
||||
search_response = await client.post(
|
||||
"/search/",
|
||||
json={"text": "test"}
|
||||
)
|
||||
search_response = await client.post("/search/", json={"text": "test"})
|
||||
search_results = SearchResponse.model_validate(search_response.json())
|
||||
assert len(search_results.results) == 1
|
||||
assert len(search_results.results) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -224,9 +171,9 @@ async def test_multiple_filters(client, indexed_entity):
|
||||
json={
|
||||
"text": "test",
|
||||
"types": [SearchItemType.ENTITY.value],
|
||||
"entity_types": [EntityType.KNOWLEDGE],
|
||||
"after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat()
|
||||
}
|
||||
"entity_types": ["test"],
|
||||
"after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat(),
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
search_result = SearchResponse.model_validate(response.json())
|
||||
@@ -234,4 +181,4 @@ async def test_multiple_filters(client, indexed_entity):
|
||||
result = search_result.results[0]
|
||||
assert result.path_id == indexed_entity.path_id
|
||||
assert result.type == SearchItemType.ENTITY.value
|
||||
assert result.metadata["entity_type"] == EntityType.KNOWLEDGE
|
||||
assert result.metadata["entity_type"] == "test"
|
||||
|
||||
+7
-6
@@ -14,7 +14,7 @@ from basic_memory.markdown.knowledge_parser import KnowledgeParser
|
||||
from basic_memory.markdown.knowledge_writer import KnowledgeWriter
|
||||
from basic_memory.markdown.note_writer import NoteWriter
|
||||
from basic_memory.models import Base
|
||||
from basic_memory.models.knowledge import Entity, EntityType
|
||||
from basic_memory.models.knowledge import Entity
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.repository.observation_repository import ObservationRepository
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
@@ -74,7 +74,6 @@ async def session_maker(engine_factory) -> async_sessionmaker[AsyncSession]:
|
||||
return session_maker
|
||||
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def entity_repository(session_maker: async_sessionmaker[AsyncSession]) -> EntityRepository:
|
||||
"""Create an EntityRepository instance."""
|
||||
@@ -129,6 +128,7 @@ def knowledge_writer():
|
||||
"""Create writer instance."""
|
||||
return KnowledgeWriter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def note_writer():
|
||||
"""Create writer instance."""
|
||||
@@ -190,7 +190,7 @@ async def sync_service(
|
||||
knowledge_sync_service: KnowledgeSyncService,
|
||||
file_change_scanner: FileChangeScanner,
|
||||
knowledge_parser: KnowledgeParser,
|
||||
search_service: SearchService
|
||||
search_service: SearchService,
|
||||
) -> SyncService:
|
||||
"""Create sync service for testing."""
|
||||
return SyncService(
|
||||
@@ -206,12 +206,12 @@ async def search_repository(session_maker):
|
||||
"""Create SearchRepository instance"""
|
||||
return SearchRepository(session_maker)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def init_search_index(search_service):
|
||||
await search_service.init_search_index()
|
||||
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def search_service(
|
||||
search_repository: SearchRepository,
|
||||
@@ -228,9 +228,10 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity:
|
||||
"""Create a sample entity for testing."""
|
||||
entity_data = {
|
||||
"name": "Test Entity",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"description": "A test entity",
|
||||
"entity_type": "test",
|
||||
"summary": "A test entity",
|
||||
"path_id": "test/test_entity",
|
||||
"file_path": "test/test_entity.md",
|
||||
"content_type": "text/markdown",
|
||||
}
|
||||
return await entity_repository.create(entity_data)
|
||||
|
||||
@@ -68,7 +68,7 @@ async def test_parse_complete_file(tmp_path, valid_entity_content):
|
||||
# Check content
|
||||
assert entity.content.title == "Auth Service"
|
||||
assert (
|
||||
entity.content.description
|
||||
entity.content.summary
|
||||
== "Core authentication service that handles user authentication."
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
from datetime import datetime, UTC
|
||||
|
||||
import pytest
|
||||
from basic_memory.models import Entity, Observation, Relation
|
||||
|
||||
from basic_memory.markdown.knowledge_writer import KnowledgeWriter
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.models import Entity, Observation, Relation
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -19,12 +19,12 @@ def sample_entity() -> Entity:
|
||||
return Entity(
|
||||
id=1,
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="knowledge/test_entity",
|
||||
file_path="knowledge/test_entity.md",
|
||||
description="Test description",
|
||||
summary="Test description",
|
||||
created_at=datetime(2025, 1, 1, tzinfo=UTC),
|
||||
updated_at=datetime(2025, 1, 2, tzinfo=UTC)
|
||||
updated_at=datetime(2025, 1, 2, tzinfo=UTC),
|
||||
)
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ def entity_with_observations(sample_entity: Entity) -> Entity:
|
||||
"""Create an entity with observations."""
|
||||
sample_entity.observations = [
|
||||
Observation(entity_id=1, category="tech", content="First observation"),
|
||||
Observation(entity_id=1, category="design", content="Second observation", context="Some context")
|
||||
Observation(
|
||||
entity_id=1, category="design", content="Second observation", context="Some context"
|
||||
),
|
||||
]
|
||||
return sample_entity
|
||||
|
||||
@@ -42,18 +44,10 @@ def entity_with_observations(sample_entity: Entity) -> Entity:
|
||||
def entity_with_relations(sample_entity: Entity) -> Entity:
|
||||
"""Create an entity with relations."""
|
||||
target = Entity(
|
||||
id=2,
|
||||
name="target_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
path_id="knowledge/target_entity"
|
||||
id=2, name="target_entity", entity_type="test", path_id="knowledge/target_entity"
|
||||
)
|
||||
sample_entity.outgoing_relations = [
|
||||
Relation(
|
||||
from_id=1,
|
||||
to_id=2,
|
||||
relation_type="connects_to",
|
||||
to_entity=target
|
||||
)
|
||||
Relation(from_id=1, to_id=2, relation_type="connects_to", to_entity=target)
|
||||
]
|
||||
return sample_entity
|
||||
|
||||
@@ -62,23 +56,22 @@ def entity_with_relations(sample_entity: Entity) -> Entity:
|
||||
async def test_format_frontmatter_basic(knowledge_writer: KnowledgeWriter, sample_entity: Entity):
|
||||
"""Test basic frontmatter formatting."""
|
||||
frontmatter = await knowledge_writer.format_frontmatter(sample_entity)
|
||||
|
||||
|
||||
assert frontmatter["id"] == "knowledge/test_entity"
|
||||
assert frontmatter["type"] == EntityType.KNOWLEDGE
|
||||
assert frontmatter["type"] == "test"
|
||||
assert frontmatter["created"] == "2025-01-01T00:00:00+00:00"
|
||||
assert frontmatter["modified"] == "2025-01-02T00:00:00+00:00"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_format_frontmatter_with_metadata(knowledge_writer: KnowledgeWriter, sample_entity: Entity):
|
||||
async def test_format_frontmatter_with_metadata(
|
||||
knowledge_writer: KnowledgeWriter, sample_entity: Entity
|
||||
):
|
||||
"""Test frontmatter includes entity metadata."""
|
||||
sample_entity.entity_metadata = {
|
||||
"status": "active",
|
||||
"priority": "high"
|
||||
}
|
||||
|
||||
sample_entity.entity_metadata = {"status": "active", "priority": "high"}
|
||||
|
||||
frontmatter = await knowledge_writer.format_frontmatter(sample_entity)
|
||||
|
||||
|
||||
assert frontmatter["status"] == "active"
|
||||
assert frontmatter["priority"] == "high"
|
||||
assert frontmatter["id"] == "knowledge/test_entity"
|
||||
@@ -89,20 +82,19 @@ async def test_format_content_basic(knowledge_writer: KnowledgeWriter, sample_en
|
||||
"""Test basic content formatting."""
|
||||
content = ""
|
||||
result = await knowledge_writer.format_content(sample_entity, content)
|
||||
|
||||
|
||||
assert "# test_entity" in result
|
||||
assert "Test description" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_format_content_with_observations(
|
||||
knowledge_writer: KnowledgeWriter,
|
||||
entity_with_observations: Entity
|
||||
knowledge_writer: KnowledgeWriter, entity_with_observations: Entity
|
||||
):
|
||||
"""Test content formatting with observations."""
|
||||
content = ""
|
||||
result = await knowledge_writer.format_content(entity_with_observations, content)
|
||||
|
||||
|
||||
assert "## Observations" in result
|
||||
assert "- [tech] First observation" in result
|
||||
assert "- [design] Second observation (Some context)" in result
|
||||
@@ -110,13 +102,12 @@ async def test_format_content_with_observations(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_format_content_with_relations(
|
||||
knowledge_writer: KnowledgeWriter,
|
||||
entity_with_relations: Entity
|
||||
knowledge_writer: KnowledgeWriter, entity_with_relations: Entity
|
||||
):
|
||||
"""Test content formatting with relations."""
|
||||
content = ""
|
||||
result = await knowledge_writer.format_content(entity_with_relations, content)
|
||||
|
||||
|
||||
assert "## Relations" in result
|
||||
assert "- connects_to [[target_entity]]" in result
|
||||
|
||||
@@ -125,18 +116,18 @@ async def test_format_content_with_relations(
|
||||
async def test_format_content_full_entity(
|
||||
knowledge_writer: KnowledgeWriter,
|
||||
entity_with_relations: Entity,
|
||||
entity_with_observations: Entity
|
||||
entity_with_observations: Entity,
|
||||
):
|
||||
"""Test content formatting with all entity features."""
|
||||
# Combine observations and relations
|
||||
entity_with_relations.observations = entity_with_observations.observations
|
||||
content = ""
|
||||
result = await knowledge_writer.format_content(entity_with_relations, content)
|
||||
|
||||
|
||||
# Verify all sections present
|
||||
assert "# test_entity" in result
|
||||
assert "Test description" in result
|
||||
assert "## Observations" in result
|
||||
assert "- [tech] First observation" in result
|
||||
assert "## Relations" in result
|
||||
assert "- connects_to [[target_entity]]" in result
|
||||
assert "- connects_to [[target_entity]]" in result
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
from datetime import datetime, UTC
|
||||
|
||||
import pytest
|
||||
from basic_memory.models import Entity
|
||||
|
||||
from basic_memory.markdown.note_writer import NoteWriter
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.models import Entity
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -19,11 +19,11 @@ def sample_note() -> Entity:
|
||||
return Entity(
|
||||
id=1,
|
||||
name="test_note",
|
||||
entity_type=EntityType.NOTE,
|
||||
entity_type="note",
|
||||
path_id="notes/test_note",
|
||||
file_path="notes/test_note.md",
|
||||
created_at=datetime(2025, 1, 1, tzinfo=UTC),
|
||||
updated_at=datetime(2025, 1, 2, tzinfo=UTC)
|
||||
updated_at=datetime(2025, 1, 2, tzinfo=UTC),
|
||||
)
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ def sample_note() -> Entity:
|
||||
async def test_format_frontmatter_basic(note_writer: NoteWriter, sample_note: Entity):
|
||||
"""Test basic frontmatter formatting."""
|
||||
frontmatter = await note_writer.format_frontmatter(sample_note)
|
||||
|
||||
|
||||
assert frontmatter["id"] == "notes/test_note"
|
||||
assert frontmatter["type"] == EntityType.NOTE
|
||||
assert frontmatter["type"] == "note"
|
||||
assert frontmatter["created"] == "2025-01-01T00:00:00+00:00"
|
||||
assert frontmatter["modified"] == "2025-01-02T00:00:00+00:00"
|
||||
|
||||
@@ -41,13 +41,10 @@ async def test_format_frontmatter_basic(note_writer: NoteWriter, sample_note: En
|
||||
@pytest.mark.asyncio
|
||||
async def test_format_frontmatter_with_metadata(note_writer: NoteWriter, sample_note: Entity):
|
||||
"""Test frontmatter includes entity metadata."""
|
||||
sample_note.entity_metadata = {
|
||||
"category": "research",
|
||||
"tags": ["python", "testing"]
|
||||
}
|
||||
|
||||
sample_note.entity_metadata = {"category": "research", "tags": ["python", "testing"]}
|
||||
|
||||
frontmatter = await note_writer.format_frontmatter(sample_note)
|
||||
|
||||
|
||||
assert frontmatter["category"] == "research"
|
||||
assert frontmatter["tags"] == ["python", "testing"]
|
||||
assert frontmatter["id"] == "notes/test_note"
|
||||
@@ -58,7 +55,7 @@ async def test_format_content_basic(note_writer: NoteWriter, sample_note: Entity
|
||||
"""Test basic content formatting."""
|
||||
content = "# Test Note\n\nThis is a test note."
|
||||
result = await note_writer.format_content(sample_note, content)
|
||||
|
||||
|
||||
assert result == content
|
||||
|
||||
|
||||
@@ -67,7 +64,7 @@ async def test_format_content_strips_whitespace(note_writer: NoteWriter, sample_
|
||||
"""Test content formatting strips extra whitespace."""
|
||||
content = "\n\n# Test Note\n\nThis is a test note.\n\n"
|
||||
result = await note_writer.format_content(sample_note, content)
|
||||
|
||||
|
||||
assert result == "# Test Note\n\nThis is a test note."
|
||||
|
||||
|
||||
@@ -86,6 +83,6 @@ This note has:
|
||||
def test():
|
||||
pass
|
||||
```"""
|
||||
|
||||
|
||||
result = await note_writer.format_content(sample_note, content)
|
||||
assert result == content
|
||||
assert result == content
|
||||
|
||||
@@ -33,7 +33,7 @@ def test_entity_data():
|
||||
{
|
||||
"name": "Test Entity",
|
||||
"entity_type": "test",
|
||||
"description": "", # Empty string instead of None
|
||||
"summary": "", # Empty string instead of None
|
||||
"observations": ["This is a test observation"],
|
||||
}
|
||||
]
|
||||
@@ -48,7 +48,7 @@ def test_directory_entity_data():
|
||||
{
|
||||
"name": "Directory Organization",
|
||||
"entity_type": "memory",
|
||||
"description": "Implemented filesystem organization by entity type",
|
||||
"summary": "Implemented filesystem organization by entity type",
|
||||
"observations": [
|
||||
"Files are now organized by type using directories like entities/project/basic_memory",
|
||||
"Entity IDs match filesystem paths for better mental model",
|
||||
|
||||
@@ -3,26 +3,25 @@
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools.knowledge import create_entities, add_observations
|
||||
from basic_memory.schemas.base import ObservationCategory, Entity, EntityType
|
||||
from basic_memory.schemas.request import CreateEntityRequest, AddObservationsRequest, ObservationCreate
|
||||
from basic_memory.schemas.base import ObservationCategory, Entity
|
||||
from basic_memory.schemas.request import (
|
||||
CreateEntityRequest,
|
||||
AddObservationsRequest,
|
||||
ObservationCreate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_basic_observation(client):
|
||||
"""Test adding a single observation with default category."""
|
||||
# First create an entity to add observations to
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
|
||||
result = await create_entities(entity_request)
|
||||
entity_id = result.entities[0].path_id
|
||||
|
||||
# Add an observation
|
||||
request = AddObservationsRequest(
|
||||
path_id=entity_id,
|
||||
observations=[
|
||||
ObservationCreate(content="Test observation")
|
||||
]
|
||||
path_id=entity_id, observations=[ObservationCreate(content="Test observation")]
|
||||
)
|
||||
updated = await add_observations(request)
|
||||
|
||||
@@ -37,9 +36,7 @@ async def test_add_basic_observation(client):
|
||||
async def test_add_categorized_observations(client):
|
||||
"""Test adding observations with different categories."""
|
||||
# Create test entity
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
|
||||
result = await create_entities(entity_request)
|
||||
entity_id = result.entities[0].path_id
|
||||
|
||||
@@ -48,23 +45,20 @@ async def test_add_categorized_observations(client):
|
||||
path_id=entity_id,
|
||||
observations=[
|
||||
ObservationCreate(
|
||||
content="Implementation uses SQLite",
|
||||
category=ObservationCategory.TECH
|
||||
content="Implementation uses SQLite", category=ObservationCategory.TECH
|
||||
),
|
||||
ObservationCreate(
|
||||
content="Chose SQLite for simplicity",
|
||||
category=ObservationCategory.DESIGN
|
||||
content="Chose SQLite for simplicity", category=ObservationCategory.DESIGN
|
||||
),
|
||||
ObservationCreate(
|
||||
content="Supports atomic operations",
|
||||
category=ObservationCategory.FEATURE
|
||||
)
|
||||
]
|
||||
content="Supports atomic operations", category=ObservationCategory.FEATURE
|
||||
),
|
||||
],
|
||||
)
|
||||
updated = await add_observations(request)
|
||||
|
||||
assert len(updated.observations) == 3
|
||||
|
||||
|
||||
# Find and verify each observation by category
|
||||
tech_obs = next(o for o in updated.observations if o.category == ObservationCategory.TECH)
|
||||
design_obs = next(o for o in updated.observations if o.category == ObservationCategory.DESIGN)
|
||||
@@ -79,9 +73,7 @@ async def test_add_categorized_observations(client):
|
||||
async def test_add_observations_with_context(client):
|
||||
"""Test adding observations with shared context."""
|
||||
# Create test entity
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
|
||||
result = await create_entities(entity_request)
|
||||
entity_id = result.entities[0].path_id
|
||||
|
||||
@@ -92,21 +84,17 @@ async def test_add_observations_with_context(client):
|
||||
context=shared_context,
|
||||
observations=[
|
||||
ObservationCreate(
|
||||
content="Decided on file format",
|
||||
category=ObservationCategory.DESIGN
|
||||
content="Decided on file format", category=ObservationCategory.DESIGN
|
||||
),
|
||||
ObservationCreate(
|
||||
content="Will use markdown",
|
||||
category=ObservationCategory.TECH
|
||||
)
|
||||
]
|
||||
ObservationCreate(content="Will use markdown", category=ObservationCategory.TECH),
|
||||
],
|
||||
)
|
||||
updated = await add_observations(request)
|
||||
|
||||
assert len(updated.observations) == 2
|
||||
for obs in updated.observations:
|
||||
# Note: context handling depends on our schema - might need adjustment
|
||||
if hasattr(obs, 'context'):
|
||||
if hasattr(obs, "context"):
|
||||
assert obs.context == shared_context
|
||||
|
||||
|
||||
@@ -116,11 +104,7 @@ async def test_add_observations_preserves_existing(client):
|
||||
# Create entity with initial observation
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
observations=["Initial observation"]
|
||||
)
|
||||
Entity(name="TestEntity", entity_type="test", observations=["Initial observation"])
|
||||
]
|
||||
)
|
||||
result = await create_entities(entity_request)
|
||||
@@ -130,11 +114,8 @@ async def test_add_observations_preserves_existing(client):
|
||||
request = AddObservationsRequest(
|
||||
path_id=entity_id,
|
||||
observations=[
|
||||
ObservationCreate(
|
||||
content="New observation",
|
||||
category=ObservationCategory.TECH
|
||||
)
|
||||
]
|
||||
ObservationCreate(content="New observation", category=ObservationCategory.TECH)
|
||||
],
|
||||
)
|
||||
updated = await add_observations(request)
|
||||
|
||||
@@ -149,28 +130,19 @@ async def test_add_observations_preserves_existing(client):
|
||||
async def test_add_multiple_observations_same_category(client):
|
||||
"""Test adding multiple observations in the same category."""
|
||||
# Create test entity
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
|
||||
result = await create_entities(entity_request)
|
||||
entity_id = result.entities[0].path_id
|
||||
|
||||
# Add multiple tech observations
|
||||
tech_observations = [
|
||||
"Uses async/await",
|
||||
"Implements SQLite backend",
|
||||
"Handles UTF-8 encoding"
|
||||
]
|
||||
|
||||
tech_observations = ["Uses async/await", "Implements SQLite backend", "Handles UTF-8 encoding"]
|
||||
|
||||
request = AddObservationsRequest(
|
||||
path_id=entity_id,
|
||||
observations=[
|
||||
ObservationCreate(
|
||||
content=obs,
|
||||
category=ObservationCategory.TECH
|
||||
)
|
||||
ObservationCreate(content=obs, category=ObservationCategory.TECH)
|
||||
for obs in tech_observations
|
||||
]
|
||||
],
|
||||
)
|
||||
updated = await add_observations(request)
|
||||
|
||||
@@ -188,13 +160,10 @@ async def test_add_observation_to_nonexistent_entity(client):
|
||||
request = AddObservationsRequest(
|
||||
path_id="test/nonexistent",
|
||||
observations=[
|
||||
ObservationCreate(
|
||||
content="This should fail",
|
||||
category=ObservationCategory.NOTE
|
||||
)
|
||||
]
|
||||
ObservationCreate(content="This should fail", category=ObservationCategory.NOTE)
|
||||
],
|
||||
)
|
||||
|
||||
# Should fail because entity doesn't exist
|
||||
with pytest.raises(Exception): # Adjust exception type based on your error handling
|
||||
await add_observations(request)
|
||||
await add_observations(request)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools.knowledge import create_entities
|
||||
from basic_memory.schemas.base import ObservationCategory, Entity, EntityType
|
||||
from basic_memory.schemas.base import ObservationCategory, Entity
|
||||
from basic_memory.schemas.request import CreateEntityRequest
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ async def test_create_basic_entity(client):
|
||||
entities=[
|
||||
Entity(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity",
|
||||
observations=["First observation"]
|
||||
entity_type="test",
|
||||
summary="A test entity",
|
||||
observations=["First observation"],
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -25,13 +25,13 @@ async def test_create_basic_entity(client):
|
||||
|
||||
# Result should be an EntityListResponse
|
||||
assert len(result.entities) == 1
|
||||
|
||||
|
||||
# Check the created entity
|
||||
entity = result.entities[0]
|
||||
assert entity.name == "TestEntity"
|
||||
assert entity.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.path_id == "test_entity"
|
||||
assert entity.description == "A test entity"
|
||||
assert entity.summary == "A test entity"
|
||||
|
||||
# Check observations
|
||||
assert len(entity.observations) == 1
|
||||
@@ -50,13 +50,9 @@ async def test_create_entity_with_multiple_observations(client):
|
||||
entities=[
|
||||
Entity(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity",
|
||||
observations=[
|
||||
"First observation",
|
||||
"Second observation",
|
||||
"Third observation"
|
||||
]
|
||||
entity_type="test",
|
||||
summary="A test entity",
|
||||
observations=["First observation", "Second observation", "Third observation"],
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -65,16 +61,12 @@ async def test_create_entity_with_multiple_observations(client):
|
||||
|
||||
entity = result.entities[0]
|
||||
assert len(entity.observations) == 3
|
||||
|
||||
|
||||
# Each observation should have:
|
||||
# - content (the observation text)
|
||||
# - category (default NOTE)
|
||||
for obs in entity.observations:
|
||||
assert obs.content in [
|
||||
"First observation",
|
||||
"Second observation",
|
||||
"Third observation"
|
||||
]
|
||||
assert obs.content in ["First observation", "Second observation", "Third observation"]
|
||||
assert obs.category == ObservationCategory.NOTE
|
||||
|
||||
|
||||
@@ -83,26 +75,18 @@ async def test_create_multiple_entities(client):
|
||||
"""Test creating multiple entities in one request."""
|
||||
request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="Entity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
observations=["Observation 1"]
|
||||
),
|
||||
Entity(
|
||||
name="Entity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
observations=["Observation 2"]
|
||||
)
|
||||
Entity(name="Entity1", entity_type="test", observations=["Observation 1"]),
|
||||
Entity(name="Entity2", entity_type="test", observations=["Observation 2"]),
|
||||
]
|
||||
)
|
||||
|
||||
result = await create_entities(request)
|
||||
assert len(result.entities) == 2
|
||||
|
||||
|
||||
# Entities should be in order
|
||||
assert result.entities[0].name == "Entity1"
|
||||
assert result.entities[1].name == "Entity2"
|
||||
|
||||
|
||||
# Each should have its observation
|
||||
assert result.entities[0].observations[0].content == "Observation 1"
|
||||
assert result.entities[1].observations[0].content == "Observation 2"
|
||||
@@ -115,8 +99,8 @@ async def test_create_entity_without_observations(client):
|
||||
entities=[
|
||||
Entity(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity without observations"
|
||||
entity_type="test",
|
||||
summary="A test entity without observations",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -131,21 +115,14 @@ async def test_create_entity_without_observations(client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_entity(client):
|
||||
"""Test creating an entity with just name and type."""
|
||||
request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="MinimalEntity",
|
||||
entity_type=EntityType.KNOWLEDGE
|
||||
)
|
||||
]
|
||||
)
|
||||
request = CreateEntityRequest(entities=[Entity(name="MinimalEntity", entity_type="test")])
|
||||
|
||||
result = await create_entities(request)
|
||||
|
||||
entity = result.entities[0]
|
||||
assert entity.name == "MinimalEntity"
|
||||
assert entity.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.path_id == "minimal_entity"
|
||||
assert entity.description is None
|
||||
assert entity.summary is None
|
||||
assert len(entity.observations) == 0
|
||||
assert len(entity.relations) == 0
|
||||
assert len(entity.relations) == 0
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools.knowledge import create_entities, create_relations
|
||||
from basic_memory.schemas.base import Relation, Entity, EntityType
|
||||
from basic_memory.schemas.base import Relation, Entity
|
||||
from basic_memory.schemas.request import CreateEntityRequest, CreateRelationsRequest
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -14,8 +13,8 @@ async def test_create_basic_relation(client):
|
||||
# First create test entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE)
|
||||
Entity(name="SourceEntity", entity_type="test"),
|
||||
Entity(name="TargetEntity", entity_type="test"),
|
||||
]
|
||||
)
|
||||
await create_entities(entity_request)
|
||||
@@ -23,21 +22,17 @@ async def test_create_basic_relation(client):
|
||||
# Create relation between them
|
||||
relation_request = CreateRelationsRequest(
|
||||
relations=[
|
||||
Relation(
|
||||
from_id="source_entity",
|
||||
to_id="target_entity",
|
||||
relation_type="depends_on"
|
||||
)
|
||||
Relation(from_id="source_entity", to_id="target_entity", relation_type="depends_on")
|
||||
]
|
||||
)
|
||||
result = await create_relations(relation_request)
|
||||
|
||||
assert len(result.entities) == 2
|
||||
|
||||
|
||||
# Find source and target entities
|
||||
source = next(e for e in result.entities if e.path_id == "source_entity")
|
||||
target = next(e for e in result.entities if e.path_id == "target_entity")
|
||||
|
||||
|
||||
# Both entities should have the relation for bi-directional navigation
|
||||
assert len(source.relations) == 1
|
||||
assert len(target.relations) == 1
|
||||
@@ -61,8 +56,8 @@ async def test_create_relation_with_context(client):
|
||||
# Create test entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="Source", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="Target", entity_type=EntityType.KNOWLEDGE)
|
||||
Entity(name="Source", entity_type="test"),
|
||||
Entity(name="Target", entity_type="test"),
|
||||
]
|
||||
)
|
||||
await create_entities(entity_request)
|
||||
@@ -73,7 +68,7 @@ async def test_create_relation_with_context(client):
|
||||
from_id="source",
|
||||
to_id="target",
|
||||
relation_type="implements",
|
||||
context="Implementation details"
|
||||
context="Implementation details",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -95,37 +90,29 @@ async def test_create_multiple_relations(client):
|
||||
# Create test entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="Entity1", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="Entity2", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="Entity3", entity_type=EntityType.KNOWLEDGE)
|
||||
Entity(name="Entity1", entity_type="test"),
|
||||
Entity(name="Entity2", entity_type="test"),
|
||||
Entity(name="Entity3", entity_type="test"),
|
||||
]
|
||||
)
|
||||
await create_entities(entity_request)
|
||||
|
||||
relation_request = CreateRelationsRequest(
|
||||
relations=[
|
||||
Relation(
|
||||
from_id="entity1",
|
||||
to_id="entity2",
|
||||
relation_type="connects_to"
|
||||
),
|
||||
Relation(
|
||||
from_id="entity2",
|
||||
to_id="entity3",
|
||||
relation_type="depends_on"
|
||||
)
|
||||
Relation(from_id="entity1", to_id="entity2", relation_type="connects_to"),
|
||||
Relation(from_id="entity2", to_id="entity3", relation_type="depends_on"),
|
||||
]
|
||||
)
|
||||
result = await create_relations(relation_request)
|
||||
|
||||
|
||||
# Should return all involved entities
|
||||
assert len(result.entities) == 3
|
||||
|
||||
|
||||
# Get entities
|
||||
entity1 = next(e for e in result.entities if e.path_id == "entity1")
|
||||
entity2 = next(e for e in result.entities if e.path_id == "entity2")
|
||||
entity3 = next(e for e in result.entities if e.path_id == "entity3")
|
||||
|
||||
|
||||
# Entity1 and Entity2 should share the connects_to relation
|
||||
assert len(entity1.relations) == 1
|
||||
assert len(entity2.relations) == 2 # Has both relations
|
||||
@@ -144,8 +131,8 @@ async def test_create_bidirectional_relations(client):
|
||||
# Create test entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="Service", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="Database", entity_type=EntityType.KNOWLEDGE)
|
||||
Entity(name="Service", entity_type="test"),
|
||||
Entity(name="Database", entity_type="test"),
|
||||
]
|
||||
)
|
||||
await create_entities(entity_request)
|
||||
@@ -153,16 +140,8 @@ async def test_create_bidirectional_relations(client):
|
||||
# Create relations in both directions
|
||||
relation_request = CreateRelationsRequest(
|
||||
relations=[
|
||||
Relation(
|
||||
from_id="service",
|
||||
to_id="database",
|
||||
relation_type="depends_on"
|
||||
),
|
||||
Relation(
|
||||
from_id="database",
|
||||
to_id="service",
|
||||
relation_type="supports"
|
||||
)
|
||||
Relation(from_id="service", to_id="database", relation_type="depends_on"),
|
||||
Relation(from_id="database", to_id="service", relation_type="supports"),
|
||||
]
|
||||
)
|
||||
result = await create_relations(relation_request)
|
||||
@@ -187,20 +166,12 @@ async def test_create_bidirectional_relations(client):
|
||||
async def test_create_relation_with_invalid_entity(client):
|
||||
"""Test creating a relation with non-existent entity fails."""
|
||||
# Create only one of the needed entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="RealEntity", entity_type=EntityType.KNOWLEDGE)
|
||||
]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="RealEntity", entity_type="test")])
|
||||
await create_entities(entity_request)
|
||||
|
||||
relation_request = CreateRelationsRequest(
|
||||
relations=[
|
||||
Relation(
|
||||
from_id="real_entity",
|
||||
to_id="non_existent_entity",
|
||||
relation_type="depends_on"
|
||||
)
|
||||
Relation(from_id="real_entity", to_id="non_existent_entity", relation_type="depends_on")
|
||||
]
|
||||
)
|
||||
|
||||
@@ -215,20 +186,16 @@ async def test_create_duplicate_relation(client):
|
||||
# Create test entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="Source", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="Target", entity_type=EntityType.KNOWLEDGE)
|
||||
Entity(name="Source", entity_type="test"),
|
||||
Entity(name="Target", entity_type="test"),
|
||||
]
|
||||
)
|
||||
await create_entities(entity_request)
|
||||
|
||||
# Create relation
|
||||
relation = Relation(
|
||||
from_id="source",
|
||||
to_id="target",
|
||||
relation_type="connects_to"
|
||||
)
|
||||
relation = Relation(from_id="source", to_id="target", relation_type="connects_to")
|
||||
relation_request = CreateRelationsRequest(relations=[relation])
|
||||
|
||||
|
||||
# Create first relation
|
||||
first_result = await create_relations(relation_request)
|
||||
assert len(first_result.entities) == 2
|
||||
@@ -237,4 +204,4 @@ async def test_create_duplicate_relation(client):
|
||||
# Attempt to create same relation again
|
||||
second_result = await create_relations(relation_request)
|
||||
# Current behavior: No entities returned when duplicate relation fails
|
||||
assert len(second_result.entities) == 0
|
||||
assert len(second_result.entities) == 0
|
||||
|
||||
@@ -5,8 +5,8 @@ import pytest
|
||||
from basic_memory.mcp.tools.discovery import (
|
||||
get_observation_categories,
|
||||
)
|
||||
from basic_memory.schemas import Entity, CreateEntityRequest, ObservationCategoryList, EntityType
|
||||
from basic_memory.mcp.tools.knowledge import create_entities, add_observations
|
||||
from basic_memory.schemas import Entity, CreateEntityRequest, ObservationCategoryList
|
||||
from basic_memory.schemas.request import ObservationCreate, AddObservationsRequest
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ async def test_get_observation_categories(client):
|
||||
entities=[
|
||||
Entity(
|
||||
name="Test Entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
observations=[],
|
||||
)
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools.knowledge import get_entity, create_entities
|
||||
from basic_memory.schemas.base import Entity, ObservationCategory, EntityType
|
||||
from basic_memory.schemas.base import Entity, ObservationCategory
|
||||
from basic_memory.schemas.request import CreateEntityRequest
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
|
||||
@@ -16,9 +16,9 @@ async def test_get_basic_entity(client):
|
||||
entities=[
|
||||
Entity(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity",
|
||||
observations=["First observation"]
|
||||
entity_type="test",
|
||||
summary="A test entity",
|
||||
observations=["First observation"],
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -30,10 +30,10 @@ async def test_get_basic_entity(client):
|
||||
|
||||
# Verify entity details
|
||||
assert entity.name == "TestEntity"
|
||||
assert entity.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.path_id == "test_entity"
|
||||
assert entity.description == "A test entity"
|
||||
|
||||
assert entity.summary == "A test entity"
|
||||
|
||||
# Check observations
|
||||
assert len(entity.observations) == 1
|
||||
obs = entity.observations[0]
|
||||
@@ -47,8 +47,8 @@ async def test_get_entity_with_relations(client):
|
||||
# Create two entities that will have a relation
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE),
|
||||
Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE)
|
||||
Entity(name="SourceEntity", entity_type="test"),
|
||||
Entity(name="TargetEntity", entity_type="test"),
|
||||
]
|
||||
)
|
||||
await create_entities(entity_request)
|
||||
@@ -57,14 +57,10 @@ async def test_get_entity_with_relations(client):
|
||||
from basic_memory.mcp.tools.knowledge import create_relations
|
||||
from basic_memory.schemas.request import CreateRelationsRequest
|
||||
from basic_memory.schemas.base import Relation
|
||||
|
||||
|
||||
relation_request = CreateRelationsRequest(
|
||||
relations=[
|
||||
Relation(
|
||||
from_id="source_entity",
|
||||
to_id="target_entity",
|
||||
relation_type="depends_on"
|
||||
)
|
||||
Relation(from_id="source_entity", to_id="target_entity", relation_type="depends_on")
|
||||
]
|
||||
)
|
||||
await create_relations(relation_request)
|
||||
@@ -83,11 +79,7 @@ async def test_get_entity_with_categorized_observations(client):
|
||||
# Create entity with categorized observations
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity with categories"
|
||||
)
|
||||
Entity(name="TestEntity", entity_type="test", summary="Test entity with categories")
|
||||
]
|
||||
)
|
||||
result = await create_entities(entity_request)
|
||||
@@ -100,19 +92,10 @@ async def test_get_entity_with_categorized_observations(client):
|
||||
obs_request = AddObservationsRequest(
|
||||
path_id=path_id,
|
||||
observations=[
|
||||
ObservationCreate(
|
||||
content="Technical detail",
|
||||
category=ObservationCategory.TECH
|
||||
),
|
||||
ObservationCreate(
|
||||
content="Design decision",
|
||||
category=ObservationCategory.DESIGN
|
||||
),
|
||||
ObservationCreate(
|
||||
content="Feature note",
|
||||
category=ObservationCategory.FEATURE
|
||||
)
|
||||
]
|
||||
ObservationCreate(content="Technical detail", category=ObservationCategory.TECH),
|
||||
ObservationCreate(content="Design decision", category=ObservationCategory.DESIGN),
|
||||
ObservationCreate(content="Feature note", category=ObservationCategory.FEATURE),
|
||||
],
|
||||
)
|
||||
await add_observations(obs_request)
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools.search import open_nodes
|
||||
from basic_memory.mcp.tools.knowledge import create_entities
|
||||
from basic_memory.mcp.tools.search import open_nodes
|
||||
from basic_memory.schemas import EntityListResponse
|
||||
from basic_memory.schemas.base import Entity, EntityType
|
||||
from basic_memory.schemas.base import Entity
|
||||
from basic_memory.schemas.request import CreateEntityRequest, OpenNodesRequest
|
||||
|
||||
|
||||
@@ -15,16 +15,8 @@ async def test_open_multiple_entities(client):
|
||||
# Create some test entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="Entity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="First test entity"
|
||||
),
|
||||
Entity(
|
||||
name="Entity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Second test entity"
|
||||
)
|
||||
Entity(name="Entity1", entity_type="test", summary="First test entity"),
|
||||
Entity(name="Entity2", entity_type="test", summary="Second test entity"),
|
||||
]
|
||||
)
|
||||
create_result = await create_entities(entity_request)
|
||||
@@ -35,14 +27,14 @@ async def test_open_multiple_entities(client):
|
||||
result = await open_nodes(request)
|
||||
assert isinstance(result, EntityListResponse)
|
||||
response = EntityListResponse.model_validate(result)
|
||||
|
||||
|
||||
# Verify we got a dictionary with both entities
|
||||
assert len(response.entities) == 2
|
||||
|
||||
|
||||
for response_entity in response.entities:
|
||||
assert response_entity.path_id in path_ids
|
||||
assert response_entity.name in ["Entity1", "Entity2"]
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_nodes_with_details(client):
|
||||
@@ -52,9 +44,9 @@ async def test_open_nodes_with_details(client):
|
||||
entities=[
|
||||
Entity(
|
||||
name="DetailedEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity with details",
|
||||
observations=["First observation", "Second observation"]
|
||||
entity_type="test",
|
||||
summary="Test entity with details",
|
||||
observations=["First observation", "Second observation"],
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -69,8 +61,8 @@ async def test_open_nodes_with_details(client):
|
||||
# Verify all details are present
|
||||
entity = response.entities[0]
|
||||
assert entity.name == "DetailedEntity"
|
||||
assert entity.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity.description == "Test entity with details"
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.summary == "Test entity with details"
|
||||
assert len(entity.observations) == 2
|
||||
|
||||
|
||||
@@ -80,16 +72,8 @@ async def test_open_nodes_with_relations(client):
|
||||
# Create related entities
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="Service",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A service"
|
||||
),
|
||||
Entity(
|
||||
name="Database",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A database"
|
||||
)
|
||||
Entity(name="Service", entity_type="test", summary="A service"),
|
||||
Entity(name="Database", entity_type="test", summary="A database"),
|
||||
]
|
||||
)
|
||||
create_result = await create_entities(entity_request)
|
||||
@@ -101,13 +85,7 @@ async def test_open_nodes_with_relations(client):
|
||||
from basic_memory.schemas.base import Relation
|
||||
|
||||
relation_request = CreateRelationsRequest(
|
||||
relations=[
|
||||
Relation(
|
||||
from_id=path_ids[0],
|
||||
to_id=path_ids[1],
|
||||
relation_type="depends_on"
|
||||
)
|
||||
]
|
||||
relations=[Relation(from_id=path_ids[0], to_id=path_ids[1], relation_type="depends_on")]
|
||||
)
|
||||
await create_relations(relation_request)
|
||||
|
||||
@@ -125,21 +103,12 @@ async def test_open_nodes_with_relations(client):
|
||||
async def test_open_nonexistent_nodes(client):
|
||||
"""Test behavior when some requested nodes don't exist."""
|
||||
# First create one real entity
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="RealEntity",
|
||||
entity_type=EntityType.KNOWLEDGE
|
||||
)
|
||||
]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="RealEntity", entity_type="test")])
|
||||
create_result = await create_entities(entity_request)
|
||||
real_path_id = create_result.entities[0].path_id
|
||||
|
||||
# Try to open both real and non-existent
|
||||
request = OpenNodesRequest(
|
||||
path_ids=[real_path_id, "nonexistent"]
|
||||
)
|
||||
request = OpenNodesRequest(path_ids=[real_path_id, "nonexistent"])
|
||||
result = await open_nodes(request)
|
||||
response = EntityListResponse.model_validate(result)
|
||||
|
||||
@@ -152,14 +121,7 @@ async def test_open_nonexistent_nodes(client):
|
||||
async def test_open_single_node(client):
|
||||
"""Test behavior with single path_id."""
|
||||
# Create an entity
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(
|
||||
name="SingleEntity",
|
||||
entity_type=EntityType.KNOWLEDGE
|
||||
)
|
||||
]
|
||||
)
|
||||
entity_request = CreateEntityRequest(entities=[Entity(name="SingleEntity", entity_type="test")])
|
||||
create_result = await create_entities(entity_request)
|
||||
path_id = create_result.entities[0].path_id
|
||||
|
||||
@@ -170,4 +132,4 @@ async def test_open_single_node(client):
|
||||
|
||||
# Should get just that entity
|
||||
assert len(response.entities) == 1
|
||||
assert path_id in response.entities[0].path_id
|
||||
assert path_id in response.entities[0].path_id
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"""Tests for the EntityRepository."""
|
||||
|
||||
from datetime import datetime, UTC
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy import select
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.models import Entity, Observation, Relation
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
|
||||
|
||||
@@ -31,17 +29,19 @@ async def related_entities(session_maker):
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
source = Entity(
|
||||
name="source",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="source/source",
|
||||
file_path="source/source.md",
|
||||
description="Source entity",
|
||||
summary="Source entity",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
target = Entity(
|
||||
name="target",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="target/target",
|
||||
file_path="target/target.md",
|
||||
description="Target entity",
|
||||
summary="Target entity",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(source)
|
||||
session.add(target)
|
||||
@@ -58,10 +58,11 @@ async def test_create_entity(entity_repository: EntityRepository):
|
||||
"""Test creating a new entity"""
|
||||
entity_data = {
|
||||
"name": "Test",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"path_id": "test/test",
|
||||
"file_path": "test/test.md",
|
||||
"description": "Test description",
|
||||
"summary": "Test description",
|
||||
"content_type": "text/markdown",
|
||||
}
|
||||
entity = await entity_repository.create(entity_data)
|
||||
|
||||
@@ -91,17 +92,19 @@ async def test_create_all(entity_repository: EntityRepository):
|
||||
entity_data = [
|
||||
{
|
||||
"name": "Test_1",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"path_id": "test/test_1",
|
||||
"file_path": "test/test_1.md",
|
||||
"description": "Test description",
|
||||
"summary": "Test description",
|
||||
"content_type": "text/markdown",
|
||||
},
|
||||
{
|
||||
"name": "Test-2",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"path_id": "test/test_2",
|
||||
"file_path": "test/test_2.md",
|
||||
"description": "Test description",
|
||||
"summary": "Test description",
|
||||
"content_type": "text/markdown",
|
||||
},
|
||||
]
|
||||
entities = await entity_repository.create_all(entity_data)
|
||||
@@ -127,10 +130,11 @@ async def test_create_entity_null_description(session_maker, entity_repository:
|
||||
"""Test creating an entity with null description"""
|
||||
entity_data = {
|
||||
"name": "Test",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"path_id": "test/test",
|
||||
"file_path": "test/test.md",
|
||||
"description": None,
|
||||
"content_type": "text/markdown",
|
||||
"summary": None,
|
||||
}
|
||||
entity = await entity_repository.create(entity_data)
|
||||
|
||||
@@ -164,7 +168,7 @@ async def test_find_by_id(entity_repository: EntityRepository, sample_entity: En
|
||||
async def test_update_entity(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test updating an entity"""
|
||||
updated = await entity_repository.update(
|
||||
sample_entity.id, {"description": "Updated description"}
|
||||
sample_entity.id, {"summary": "Updated description"}
|
||||
)
|
||||
assert updated is not None
|
||||
assert updated.summary == "Updated description"
|
||||
@@ -182,7 +186,7 @@ async def test_update_entity(entity_repository: EntityRepository, sample_entity:
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_entity_to_null(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test updating an entity's description to null"""
|
||||
updated = await entity_repository.update(sample_entity.id, {"description": None})
|
||||
updated = await entity_repository.update(sample_entity.id, {"summary": None})
|
||||
assert updated is not None
|
||||
assert updated.summary is None
|
||||
|
||||
@@ -269,8 +273,6 @@ async def test_delete_nonexistent_entity(entity_repository: EntityRepository):
|
||||
assert result is False
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_entities(session_maker):
|
||||
"""Create multiple test entities."""
|
||||
@@ -278,24 +280,27 @@ async def test_entities(session_maker):
|
||||
entities = [
|
||||
Entity(
|
||||
name="entity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="First test entity",
|
||||
entity_type="test",
|
||||
summary="First test entity",
|
||||
path_id="type1/entity1",
|
||||
file_path="type1/entity1.md",
|
||||
content_type= "text/markdown",
|
||||
),
|
||||
Entity(
|
||||
name="entity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Second test entity",
|
||||
entity_type="test",
|
||||
summary="Second test entity",
|
||||
path_id="type1/entity2",
|
||||
file_path="type1/entity2.md",
|
||||
content_type="text/markdown",
|
||||
),
|
||||
Entity(
|
||||
name="entity3",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Third test entity",
|
||||
entity_type="test",
|
||||
summary="Third test entity",
|
||||
path_id="type2/entity3",
|
||||
file_path="type2/entity3.md",
|
||||
content_type="text/markdown",
|
||||
),
|
||||
]
|
||||
session.add_all(entities)
|
||||
@@ -384,31 +389,34 @@ async def test_delete_by_path_ids_with_observations(
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_entities_with_related(entity_repository: EntityRepository, session_maker):
|
||||
"""Test listing entities with related entities included."""
|
||||
|
||||
|
||||
# Create test entities
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
# Core entities
|
||||
core = Entity(
|
||||
name="core_service",
|
||||
entity_type=EntityType.NOTE,
|
||||
entity_type="note",
|
||||
path_id="service/core",
|
||||
file_path="service/core.md",
|
||||
description="Core service"
|
||||
summary="Core service",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
dbe = Entity(
|
||||
name="db_service",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="service/db",
|
||||
file_path="service/db.md",
|
||||
description="Database service"
|
||||
summary="Database service",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
# Related entity of different type
|
||||
config = Entity(
|
||||
name="service_config",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="config/service",
|
||||
file_path="config/service.md",
|
||||
description="Service configuration"
|
||||
summary="Service configuration",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add_all([core, dbe, config])
|
||||
await session.flush()
|
||||
@@ -418,23 +426,19 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s
|
||||
# core -> db (depends_on)
|
||||
Relation(from_id=core.id, to_id=dbe.id, relation_type="depends_on"),
|
||||
# config -> core (configures)
|
||||
Relation(from_id=config.id, to_id=core.id, relation_type="configures")
|
||||
Relation(from_id=config.id, to_id=core.id, relation_type="configures"),
|
||||
]
|
||||
session.add_all(relations)
|
||||
|
||||
# Test 1: List without related entities
|
||||
services = await entity_repository.list_entities(
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
include_related=False
|
||||
)
|
||||
services = await entity_repository.list_entities(entity_type="test", include_related=False)
|
||||
assert len(services) == 2
|
||||
service_names = {s.name for s in services}
|
||||
assert service_names == {"service_config", "db_service"}
|
||||
|
||||
# Test 2: List services with related entities
|
||||
services_and_related = await entity_repository.list_entities(
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
include_related=True
|
||||
entity_type="test", include_related=True
|
||||
)
|
||||
assert len(services_and_related) == 3
|
||||
# Should include both services and the config
|
||||
@@ -444,4 +448,4 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s
|
||||
# Test 3: Verify relations are loaded
|
||||
core_service = next(e for e in services_and_related if e.name == "core_service")
|
||||
assert len(core_service.outgoing_relations) > 0 # Has incoming relation from config
|
||||
assert len(core_service.incoming_relations) > 0 # Has outgoing relation to db
|
||||
assert len(core_service.incoming_relations) > 0 # Has outgoing relation to db
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.models import Entity, Observation
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.repository.observation_repository import ObservationRepository
|
||||
|
||||
|
||||
@@ -90,10 +89,11 @@ async def test_delete_observations(session_maker: async_sessionmaker, repo):
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
entity = Entity(
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
path_id="test/test_entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
@@ -119,10 +119,11 @@ async def test_delete_observation_by_id(session_maker: async_sessionmaker, repo)
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
entity = Entity(
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
path_id="test/test_entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
@@ -147,10 +148,11 @@ async def test_delete_observation_by_content(session_maker: async_sessionmaker,
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
entity = Entity(
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
path_id="test/test_entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
@@ -177,10 +179,11 @@ async def test_find_by_category(session_maker: async_sessionmaker, repo):
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
entity = Entity(
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
path_id="test/test_entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
@@ -218,10 +221,11 @@ async def test_observation_categories(session_maker: async_sessionmaker, repo):
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
entity = Entity(
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
path_id="test/test_entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
@@ -265,10 +269,11 @@ async def test_find_by_category_case_sensitivity(session_maker: async_sessionmak
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
entity = Entity(
|
||||
name="test_entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
path_id="test/test_entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
|
||||
@@ -6,7 +6,6 @@ import sqlalchemy
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.models import Entity, Relation
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
|
||||
|
||||
@@ -15,10 +14,11 @@ async def source_entity(session_maker):
|
||||
"""Create a source entity for testing relations."""
|
||||
entity = Entity(
|
||||
name="test_source",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="source/test_source",
|
||||
file_path="source/test_source.md",
|
||||
description="Source entity",
|
||||
summary="Source entity",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
session.add(entity)
|
||||
@@ -31,10 +31,11 @@ async def target_entity(session_maker):
|
||||
"""Create a target entity for testing relations."""
|
||||
entity = Entity(
|
||||
name="test_target",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="target/test_target",
|
||||
file_path="target/test_target.md",
|
||||
description="Target entity",
|
||||
summary="Target entity",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
session.add(entity)
|
||||
@@ -60,10 +61,11 @@ async def related_entity(entity_repository):
|
||||
"""Create a second entity for testing relations"""
|
||||
entity_data = {
|
||||
"name": "Related Entity",
|
||||
"entity_type": EntityType.KNOWLEDGE,
|
||||
"entity_type": "test",
|
||||
"path_id": "test/related_entity",
|
||||
"file_path": "test/related_entity.md",
|
||||
"description": "A related test entity",
|
||||
"summary": "A related test entity",
|
||||
"content_type": "text/markdown",
|
||||
"references": "",
|
||||
}
|
||||
return await entity_repository.create(entity_data)
|
||||
@@ -158,12 +160,15 @@ async def test_find_by_entities(
|
||||
assert relations[0].id == sample_relation.id
|
||||
assert relations[0].relation_type == sample_relation.relation_type
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_relation(relation_repository: RelationRepository, sample_relation: Relation):
|
||||
"""Test finding relations by type"""
|
||||
relation = await relation_repository.find_relation(from_path_id=sample_relation.from_entity.path_id,
|
||||
to_path_id=sample_relation.to_entity.path_id,
|
||||
relation_type=sample_relation.relation_type)
|
||||
relation = await relation_repository.find_relation(
|
||||
from_path_id=sample_relation.from_entity.path_id,
|
||||
to_path_id=sample_relation.to_entity.path_id,
|
||||
relation_type=sample_relation.relation_type,
|
||||
)
|
||||
assert relation.id == sample_relation.id
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_entity_in_minimal():
|
||||
entity = Entity.model_validate(data)
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.entity_type == "knowledge"
|
||||
assert entity.description is None
|
||||
assert entity.summary is None
|
||||
assert entity.observations == []
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ def test_entity_in_complete():
|
||||
data = {
|
||||
"name": "test_entity",
|
||||
"entity_type": "knowledge",
|
||||
"description": "A test entity",
|
||||
"summary": "A test entity",
|
||||
"observations": ["Test observation"],
|
||||
}
|
||||
entity = Entity.model_validate(data)
|
||||
assert entity.name == "test_entity"
|
||||
assert entity.entity_type == "knowledge"
|
||||
assert entity.description == "A test entity"
|
||||
assert entity.summary == "A test entity"
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0] == "Test observation"
|
||||
|
||||
@@ -43,7 +43,7 @@ def test_entity_in_complete():
|
||||
def test_entity_in_validation():
|
||||
"""Test validation errors for EntityIn."""
|
||||
with pytest.raises(ValidationError):
|
||||
Entity.model_validate({}) # Missing required fields
|
||||
Entity.model_validate({"file_path": "test"}) # Missing required fields
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
Entity.model_validate({"name": "test"}) # Missing entityType
|
||||
@@ -85,12 +85,12 @@ def test_create_entities_input():
|
||||
data = {
|
||||
"entities": [
|
||||
{"name": "entity1", "entity_type": "knowledge"},
|
||||
{"name": "entity2", "entity_type": "knowledge", "description": "test description"},
|
||||
{"name": "entity2", "entity_type": "knowledge", "summary": "test description"},
|
||||
]
|
||||
}
|
||||
create_input = CreateEntityRequest.model_validate(data)
|
||||
assert len(create_input.entities) == 2
|
||||
assert create_input.entities[1].description == "test description"
|
||||
assert create_input.entities[1].summary == "test description"
|
||||
|
||||
# Empty entities list should fail
|
||||
with pytest.raises(ValidationError):
|
||||
@@ -104,7 +104,8 @@ def test_entity_out_from_attributes():
|
||||
"path_id": "test/test",
|
||||
"name": "test",
|
||||
"entity_type": "knowledge",
|
||||
"description": "test description",
|
||||
"content_type": "text/markdown",
|
||||
"summary": "test description",
|
||||
"observations": [{"id": 1, "content": "test obs", "context": None}],
|
||||
"relations": [
|
||||
{"id": 1, "from_id": "test/test", "to_id": "test/test", "relation_type": "test", "context": None}
|
||||
@@ -112,7 +113,7 @@ def test_entity_out_from_attributes():
|
||||
}
|
||||
entity = EntityResponse.model_validate(db_data)
|
||||
assert entity.path_id == "test/test"
|
||||
assert entity.description == "test description"
|
||||
assert entity.summary == "test description"
|
||||
assert len(entity.observations) == 1
|
||||
assert len(entity.relations) == 1
|
||||
|
||||
@@ -121,7 +122,7 @@ def test_optional_fields():
|
||||
"""Test handling of optional fields."""
|
||||
# Create with no optional fields
|
||||
entity = Entity.model_validate({"name": "test", "entity_type": "knowledge"})
|
||||
assert entity.description is None
|
||||
assert entity.summary is None
|
||||
assert entity.observations == []
|
||||
|
||||
# Create with empty optional fields
|
||||
@@ -129,18 +130,18 @@ def test_optional_fields():
|
||||
{
|
||||
"name": "test",
|
||||
"entity_type": "knowledge",
|
||||
"description": None,
|
||||
"summary": None,
|
||||
"observations": [],
|
||||
}
|
||||
)
|
||||
assert entity.description is None
|
||||
assert entity.summary is None
|
||||
assert entity.observations == []
|
||||
|
||||
# Create with some optional fields
|
||||
entity = Entity.model_validate(
|
||||
{"name": "test", "entity_type": "knowledge", "description": "test", "observations": []}
|
||||
{"name": "test", "entity_type": "knowledge", "summary": "test", "observations": []}
|
||||
)
|
||||
assert entity.description == "test"
|
||||
assert entity.summary == "test"
|
||||
assert entity.observations == []
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.schemas import Entity as EntitySchema, EntityType
|
||||
from basic_memory.schemas import Entity as EntitySchema
|
||||
from basic_memory.services.entity_service import EntityService
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
|
||||
@@ -29,8 +29,8 @@ async def test_create_entity(entity_service: EntityService):
|
||||
"""Test successful entity creation."""
|
||||
entity_data = EntitySchema(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity description",
|
||||
entity_type="test",
|
||||
summary="A test entity description",
|
||||
observations=["this is a test observation"],
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_create_entity(entity_service: EntityService):
|
||||
assert entity.name == "TestEntity"
|
||||
assert entity.path_id == entity_data.path_id
|
||||
assert entity.file_path == entity_data.file_path
|
||||
assert entity.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.summary == "A test entity description"
|
||||
assert entity.created_at is not None
|
||||
assert entity.observations[0].content == "this is a test observation"
|
||||
@@ -52,7 +52,7 @@ async def test_create_entity(entity_service: EntityService):
|
||||
retrieved = await entity_service.get_by_path_id(entity_data.path_id)
|
||||
assert retrieved.summary == "A test entity description"
|
||||
assert retrieved.name == "TestEntity"
|
||||
assert retrieved.entity_type == EntityType.KNOWLEDGE
|
||||
assert retrieved.entity_type == "test"
|
||||
assert retrieved.summary == "A test entity description"
|
||||
assert retrieved.created_at is not None
|
||||
assert retrieved.observations[0].content == "this is a test observation"
|
||||
@@ -63,14 +63,14 @@ async def test_create_entities(entity_service: EntityService):
|
||||
entity_data = [
|
||||
EntitySchema(
|
||||
name="TestEntity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity description",
|
||||
entity_type="test",
|
||||
summary="A test entity description",
|
||||
observations=["this is a test observation"],
|
||||
),
|
||||
EntitySchema(
|
||||
name="TestEntity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="A test entity description",
|
||||
entity_type="test",
|
||||
summary="A test entity description",
|
||||
observations=["this is a test observation"],
|
||||
),
|
||||
]
|
||||
@@ -83,7 +83,7 @@ async def test_create_entities(entity_service: EntityService):
|
||||
entity1 = entities[0]
|
||||
assert isinstance(entity1, EntityModel)
|
||||
assert entity1.name == "TestEntity1"
|
||||
assert entity1.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity1.entity_type == "test"
|
||||
assert entity1.summary == "A test entity description"
|
||||
assert entity1.created_at is not None
|
||||
assert entity1.observations[0].content == "this is a test observation"
|
||||
@@ -92,7 +92,7 @@ async def test_create_entities(entity_service: EntityService):
|
||||
entity2 = entities[1]
|
||||
assert isinstance(entity1, EntityModel)
|
||||
assert entity2.name == "TestEntity2"
|
||||
assert entity2.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity2.entity_type == "test"
|
||||
assert entity2.summary == "A test entity description"
|
||||
assert entity2.created_at is not None
|
||||
assert entity2.observations[0].content == "this is a test observation"
|
||||
@@ -109,16 +109,16 @@ async def test_get_by_path_id(entity_service: EntityService):
|
||||
"""Test finding entity by type and name combination."""
|
||||
entity1_data = EntitySchema(
|
||||
name="TestEntity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="First test entity",
|
||||
entity_type="test",
|
||||
summary="First test entity",
|
||||
observations=[],
|
||||
)
|
||||
entity1 = await entity_service.create_entity(entity1_data)
|
||||
|
||||
entity2_data = EntitySchema(
|
||||
name="TestEntity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Second test entity",
|
||||
name="TestEntity2",
|
||||
entity_type="test",
|
||||
summary="Second test entity",
|
||||
observations=[],
|
||||
)
|
||||
entity2 = await entity_service.create_entity(entity2_data)
|
||||
@@ -144,7 +144,7 @@ async def test_get_by_path_id(entity_service: EntityService):
|
||||
|
||||
async def test_create_entity_no_description(entity_service: EntityService):
|
||||
"""Test creating entity without description (should be None)."""
|
||||
entity_data = EntitySchema(name="TestEntity", entity_type=EntityType.KNOWLEDGE, observations=[])
|
||||
entity_data = EntitySchema(name="TestEntity", entity_type="test", observations=[])
|
||||
|
||||
entity = await entity_service.create_entity(entity_data)
|
||||
assert entity.summary is None
|
||||
@@ -158,8 +158,8 @@ async def test_get_entity_success(entity_service: EntityService):
|
||||
"""Test successful entity retrieval."""
|
||||
entity_data = EntitySchema(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test description",
|
||||
entity_type="test",
|
||||
summary="Test description",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
@@ -169,7 +169,7 @@ async def test_get_entity_success(entity_service: EntityService):
|
||||
|
||||
assert isinstance(retrieved, EntityModel)
|
||||
assert retrieved.name == "TestEntity"
|
||||
assert retrieved.entity_type == EntityType.KNOWLEDGE
|
||||
assert retrieved.entity_type == "test"
|
||||
assert retrieved.summary == "Test description"
|
||||
|
||||
|
||||
@@ -177,15 +177,15 @@ async def test_update_entity_description(entity_service: EntityService):
|
||||
"""Test updating an entity's description."""
|
||||
entity_data = EntitySchema(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Initial description",
|
||||
entity_type="test",
|
||||
summary="Initial description",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
|
||||
# Update description using path_id
|
||||
updated = await entity_service.update_entity(
|
||||
entity_data.path_id, {"description": "Updated description"}
|
||||
entity_data.path_id, {"summary": "Updated description"}
|
||||
)
|
||||
assert updated.summary == "Updated description"
|
||||
|
||||
@@ -198,14 +198,14 @@ async def test_update_entity_description_to_none(entity_service: EntityService):
|
||||
"""Test updating an entity's description to None."""
|
||||
entity_data = EntitySchema(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Initial description",
|
||||
entity_type="test",
|
||||
summary="Initial description",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
|
||||
# Update description to None using path_id
|
||||
updated = await entity_service.update_entity(entity_data.path_id, {"description": None})
|
||||
updated = await entity_service.update_entity(entity_data.path_id, {"summary": None})
|
||||
assert updated.summary is None
|
||||
|
||||
# Verify after retrieval
|
||||
@@ -217,7 +217,7 @@ async def test_delete_entity_success(entity_service: EntityService):
|
||||
"""Test successful entity deletion."""
|
||||
entity_data = EntitySchema(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
@@ -249,8 +249,8 @@ async def test_create_entity_with_special_chars(entity_service: EntityService):
|
||||
description = "Description with $pecial chars & symbols!"
|
||||
entity_data = EntitySchema(
|
||||
name=name,
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description=description,
|
||||
entity_type="test",
|
||||
summary=description,
|
||||
)
|
||||
entity = await entity_service.create_entity(entity_data)
|
||||
|
||||
@@ -267,8 +267,8 @@ async def test_create_entity_long_description(entity_service: EntityService):
|
||||
long_description = "A" * 1000 # 1000 character description
|
||||
entity_data = EntitySchema(
|
||||
name="TestEntity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description=long_description,
|
||||
entity_type="test",
|
||||
summary=long_description,
|
||||
observations=[],
|
||||
)
|
||||
|
||||
@@ -285,14 +285,14 @@ async def test_open_nodes_by_path_ids(entity_service: EntityService):
|
||||
# Create test entities
|
||||
entity1_data = EntitySchema(
|
||||
name="Entity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="First entity",
|
||||
entity_type="test",
|
||||
summary="First entity",
|
||||
observations=[],
|
||||
)
|
||||
entity2_data = EntitySchema(
|
||||
name="Entity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Second entity",
|
||||
entity_type="test",
|
||||
summary="Second entity",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity1_data)
|
||||
@@ -318,8 +318,8 @@ async def test_open_nodes_some_not_found(entity_service: EntityService):
|
||||
# Create one test entity
|
||||
entity_data = EntitySchema(
|
||||
name="Entity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
@@ -337,14 +337,14 @@ async def test_delete_entities_by_path_ids(entity_service: EntityService):
|
||||
# Create test entities
|
||||
entity1_data = EntitySchema(
|
||||
name="Entity1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="First entity",
|
||||
entity_type="test",
|
||||
summary="First entity",
|
||||
observations=[],
|
||||
)
|
||||
entity2_data = EntitySchema(
|
||||
name="Entity2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Second entity",
|
||||
entity_type="test",
|
||||
summary="Second entity",
|
||||
observations=[],
|
||||
)
|
||||
await entity_service.create_entity(entity1_data)
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
import yaml
|
||||
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.models.knowledge import EntityType, ObservationCategory
|
||||
from basic_memory.models.knowledge import ObservationCategory
|
||||
from basic_memory.schemas import Entity as EntitySchema, Relation as RelationSchema
|
||||
from basic_memory.schemas.request import ObservationCreate
|
||||
from basic_memory.services import EntityService
|
||||
@@ -20,8 +20,8 @@ async def test_get_entity_path(knowledge_service: KnowledgeService):
|
||||
id=1,
|
||||
path_id="test-entity",
|
||||
name="test-entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
)
|
||||
path = knowledge_service.get_entity_path(entity)
|
||||
assert path == Path(knowledge_service.base_path / "test-entity.md")
|
||||
@@ -31,9 +31,7 @@ async def test_get_entity_path(knowledge_service: KnowledgeService):
|
||||
async def test_create_entity(knowledge_service: KnowledgeService):
|
||||
"""Should create entity in DB and write file correctly."""
|
||||
# Setup
|
||||
entity_schema = EntitySchema(
|
||||
name="test-entity", entity_type=EntityType.KNOWLEDGE, description="Test entity"
|
||||
)
|
||||
entity_schema = EntitySchema(name="test-entity", entity_type="test", summary="Test entity")
|
||||
|
||||
# Execute
|
||||
created = await knowledge_service.create_entity(entity_schema)
|
||||
@@ -41,7 +39,7 @@ async def test_create_entity(knowledge_service: KnowledgeService):
|
||||
# Verify DB entity
|
||||
assert created.name == entity_schema.name
|
||||
assert created.entity_type == entity_schema.entity_type
|
||||
assert created.summary == entity_schema.description
|
||||
assert created.summary == entity_schema.summary
|
||||
assert created.checksum is not None
|
||||
assert created.path_id == "test_entity"
|
||||
assert created.file_path == "test_entity.md"
|
||||
@@ -65,9 +63,7 @@ async def test_create_entity(knowledge_service: KnowledgeService):
|
||||
async def test_create_multiple_entities(knowledge_service: KnowledgeService):
|
||||
"""Should create multiple entities successfully."""
|
||||
entities = [
|
||||
EntitySchema(
|
||||
name=f"entity-{i}", entity_type=EntityType.KNOWLEDGE, description=f"Test entity {i}"
|
||||
)
|
||||
EntitySchema(name=f"entity-{i}", entity_type="test", summary=f"Test entity {i}")
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
@@ -85,10 +81,10 @@ async def test_create_relations(knowledge_service: KnowledgeService, entity_serv
|
||||
"""Should create relations and update related entity files."""
|
||||
# Create test entities
|
||||
entity1 = await knowledge_service.create_entity(
|
||||
EntitySchema(name="entity1", entity_type=EntityType.KNOWLEDGE, description="Test entity 1")
|
||||
EntitySchema(name="entity1", entity_type="test", summary="Test entity 1")
|
||||
)
|
||||
entity2 = await knowledge_service.create_entity(
|
||||
EntitySchema(name="entity2", entity_type=EntityType.KNOWLEDGE, description="Test entity 2")
|
||||
EntitySchema(name="entity2", entity_type="test", summary="Test entity 2")
|
||||
)
|
||||
|
||||
# Create relation
|
||||
@@ -124,15 +120,15 @@ async def test_update_knowledge_entity_description(knowledge_service: KnowledgeS
|
||||
entity = await knowledge_service.create_entity(
|
||||
EntitySchema(
|
||||
name="test",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
)
|
||||
|
||||
# Update description
|
||||
updated = await knowledge_service.update_entity(
|
||||
entity.path_id, description="Updated description"
|
||||
entity.path_id, summary="Updated description"
|
||||
)
|
||||
|
||||
# Verify file has new description but preserved metadata
|
||||
@@ -154,8 +150,8 @@ async def test_update_note_entity_content(knowledge_service: KnowledgeService):
|
||||
entity = await knowledge_service.create_entity(
|
||||
EntitySchema(
|
||||
name="test",
|
||||
entity_type=EntityType.NOTE,
|
||||
description="Test note",
|
||||
entity_type="note",
|
||||
summary="Test note",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
)
|
||||
@@ -184,8 +180,8 @@ async def test_update_entity_name(knowledge_service: KnowledgeService):
|
||||
entity = await knowledge_service.create_entity(
|
||||
EntitySchema(
|
||||
name="test",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test entity",
|
||||
entity_type="test",
|
||||
summary="Test entity",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
)
|
||||
@@ -216,26 +212,20 @@ async def test_update_entity_type_note_to_knowledge(knowledge_service: Knowledge
|
||||
entity = await knowledge_service.create_entity(
|
||||
EntitySchema(
|
||||
name="test",
|
||||
entity_type=EntityType.NOTE,
|
||||
description="Test note",
|
||||
entity_metadata={"status": "draft"}
|
||||
entity_type="note",
|
||||
summary="Test note",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
)
|
||||
|
||||
# First update with some content as a note
|
||||
await knowledge_service.update_entity(
|
||||
entity.path_id,
|
||||
content=initial_content
|
||||
)
|
||||
await knowledge_service.update_entity(entity.path_id, content=initial_content)
|
||||
|
||||
# Then update to knowledge type
|
||||
updated = await knowledge_service.update_entity(
|
||||
entity.path_id,
|
||||
entity_type=EntityType.KNOWLEDGE
|
||||
)
|
||||
updated = await knowledge_service.update_entity(entity.path_id, entity_type="test")
|
||||
|
||||
# Verify type was updated in DB
|
||||
assert updated.entity_type == EntityType.KNOWLEDGE
|
||||
assert updated.entity_type == "test"
|
||||
|
||||
# Verify frontmatter was updated
|
||||
file_path = knowledge_service.get_entity_path(updated)
|
||||
@@ -243,7 +233,7 @@ async def test_update_entity_type_note_to_knowledge(knowledge_service: Knowledge
|
||||
|
||||
_, frontmatter, _ = content.split("---", 2)
|
||||
metadata = yaml.safe_load(frontmatter)
|
||||
assert metadata["type"] == EntityType.KNOWLEDGE
|
||||
assert metadata["type"] == "test"
|
||||
|
||||
# Verify content format changed to knowledge style (structured)
|
||||
assert "# test" in content
|
||||
@@ -257,9 +247,9 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
|
||||
entity = await knowledge_service.create_entity(
|
||||
EntitySchema(
|
||||
name="test",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
description="Test knowledge entity",
|
||||
entity_metadata={"status": "draft"}
|
||||
entity_type="test",
|
||||
summary="Test knowledge entity",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -272,13 +262,11 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
|
||||
# Update to note type with new content
|
||||
new_content = "# Test Note\n\nConverted to note format."
|
||||
updated = await knowledge_service.update_entity(
|
||||
entity.path_id,
|
||||
entity_type=EntityType.NOTE,
|
||||
content=new_content
|
||||
entity.path_id, entity_type="note", content=new_content
|
||||
)
|
||||
|
||||
# Verify type was updated in DB
|
||||
assert updated.entity_type == EntityType.NOTE
|
||||
assert updated.entity_type == "note"
|
||||
|
||||
# Verify frontmatter was updated
|
||||
file_path = knowledge_service.get_entity_path(updated)
|
||||
@@ -286,7 +274,7 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
|
||||
|
||||
_, frontmatter, _ = content.split("---", 2)
|
||||
metadata = yaml.safe_load(frontmatter)
|
||||
assert metadata["type"] == EntityType.NOTE
|
||||
assert metadata["type"] == "note"
|
||||
|
||||
# Verify content changed to note style (direct content)
|
||||
assert "# Test Note" in content
|
||||
@@ -294,4 +282,4 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
|
||||
assert "Test observation" not in content # Observations not included in note format
|
||||
|
||||
# Verify metadata was preserved
|
||||
assert metadata["status"] == "draft"
|
||||
assert metadata["status"] == "draft"
|
||||
|
||||
@@ -5,12 +5,9 @@ import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from basic_memory.models import Entity, Relation
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
from basic_memory.services.relation_service import RelationService
|
||||
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_entities(
|
||||
session_maker: async_sessionmaker[AsyncSession],
|
||||
@@ -19,17 +16,19 @@ async def test_entities(
|
||||
async with session_maker() as session:
|
||||
entity1 = Entity(
|
||||
name="test_entity_1",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="test/test_entity_1",
|
||||
file_path="test/test_entity_1.md",
|
||||
description="Test entity 1",
|
||||
summary="Test entity 1",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
entity2 = Entity(
|
||||
name="test_entity_2",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="test/test_entity_2",
|
||||
file_path="test/test_entity_2.md",
|
||||
description="Test entity 2",
|
||||
summary="Test entity 2",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
session.add_all([entity1, entity2])
|
||||
await session.commit()
|
||||
|
||||
@@ -19,7 +19,8 @@ def test_entity():
|
||||
entity_metadata = { "test": "test"}
|
||||
path_id = "component/test_component"
|
||||
file_path = "entities/component/test_component.md"
|
||||
description = "A test component for search"
|
||||
summary = "A test component for search"
|
||||
content_type = "text/markdown"
|
||||
created_at = datetime.now(timezone.utc)
|
||||
updated_at = datetime.now(timezone.utc)
|
||||
observations = []
|
||||
@@ -97,7 +98,7 @@ async def test_update_index(search_service, test_entity):
|
||||
await search_service.index_entity(test_entity)
|
||||
|
||||
# Update entity
|
||||
test_entity.description = "Updated description with new terms"
|
||||
test_entity.summary = "Updated description with new terms"
|
||||
await search_service.index_entity(test_entity)
|
||||
|
||||
# Search for new terms
|
||||
|
||||
@@ -14,7 +14,6 @@ from basic_memory.markdown.schemas import (
|
||||
Relation as MarkdownRelation,
|
||||
)
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.sync.knowledge_sync_service import KnowledgeSyncService
|
||||
|
||||
|
||||
@@ -35,7 +34,7 @@ def test_content() -> EntityContent:
|
||||
"""Create test content with observations and relations."""
|
||||
return EntityContent(
|
||||
title="Test Entity",
|
||||
description="A test entity description",
|
||||
summary="A test entity description",
|
||||
observations=[
|
||||
MarkdownObservation(content="First observation"),
|
||||
MarkdownObservation(content="Second observation"),
|
||||
@@ -65,7 +64,7 @@ async def test_create_entity_without_relations(
|
||||
|
||||
# Check basic fields
|
||||
assert entity.name == "Test Entity"
|
||||
assert entity.entity_type == EntityType.KNOWLEDGE
|
||||
assert entity.entity_type == "knowledge"
|
||||
assert entity.path_id == "concept/test_entity"
|
||||
assert entity.summary == "A test entity description"
|
||||
|
||||
@@ -91,7 +90,7 @@ async def test_update_entity_without_relations(
|
||||
|
||||
# Modify markdown content
|
||||
test_markdown.content.title = "Updated Title"
|
||||
test_markdown.content.description = "Updated description"
|
||||
test_markdown.content.summary = "Updated description"
|
||||
test_markdown.content.observations = [MarkdownObservation(content="Updated observation")]
|
||||
|
||||
# Update entity
|
||||
@@ -120,15 +119,17 @@ async def test_update_entity_relations(
|
||||
# Create target entities that relations point to
|
||||
other_entity = EntityModel(
|
||||
name="Other Entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="concept/other_entity",
|
||||
file_path="concept/other_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
another_entity = EntityModel(
|
||||
name="Another Entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="concept/another_entity",
|
||||
file_path="concept/another_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
await knowledge_sync_service.entity_service.add(other_entity)
|
||||
await knowledge_sync_service.entity_service.add(another_entity)
|
||||
@@ -163,15 +164,17 @@ async def test_two_pass_sync_flow(
|
||||
# Create target entities first
|
||||
other_entity = EntityModel(
|
||||
name="Other Entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="concept/other_entity",
|
||||
file_path="concept/other_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
another_entity = EntityModel(
|
||||
name="Another Entity",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
path_id="concept/another_entity",
|
||||
file_path="concept/another_entity.md",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
await knowledge_sync_service.entity_service.add(other_entity)
|
||||
await knowledge_sync_service.entity_service.add(another_entity)
|
||||
|
||||
@@ -6,7 +6,6 @@ import pytest
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.services import EntityService
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
|
||||
@@ -49,9 +48,10 @@ A test concept.
|
||||
other = Entity(
|
||||
path_id="concept/other",
|
||||
name="Other",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
file_path="concept/other.md",
|
||||
checksum="12345678",
|
||||
content_type="text/markdown",
|
||||
)
|
||||
await entity_service.repository.add(other)
|
||||
|
||||
@@ -64,7 +64,7 @@ A test concept.
|
||||
|
||||
# Find new entity
|
||||
test_concept = next(e for e in entities if e.path_id == "concept/test_concept")
|
||||
assert test_concept.entity_type == EntityType.KNOWLEDGE
|
||||
assert test_concept.entity_type == "knowledge"
|
||||
|
||||
# Verify relation was not created
|
||||
# because file for related entity was not found
|
||||
|
||||
@@ -7,7 +7,6 @@ import pytest
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.models.knowledge import EntityType
|
||||
from basic_memory.services import EntityService
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
|
||||
@@ -34,7 +33,9 @@ async def test_sync_file_modified_during_sync(
|
||||
"""Test handling of files that change during sync process."""
|
||||
# Create initial files
|
||||
doc_path = test_config.knowledge_dir / "changing.md"
|
||||
await create_test_file(doc_path, """
|
||||
await create_test_file(
|
||||
doc_path,
|
||||
"""
|
||||
---
|
||||
type: knowledge
|
||||
id: changing
|
||||
@@ -45,7 +46,8 @@ modified: 2024-01-01
|
||||
|
||||
## Observations
|
||||
- This is a test
|
||||
""")
|
||||
""",
|
||||
)
|
||||
|
||||
# Setup async modification during sync
|
||||
async def modify_file():
|
||||
@@ -71,9 +73,10 @@ async def test_sync_null_checksum_cleanup(
|
||||
entity = Entity(
|
||||
path_id="concept/incomplete",
|
||||
name="Incomplete",
|
||||
entity_type=EntityType.KNOWLEDGE,
|
||||
entity_type="test",
|
||||
file_path="concept/incomplete.md",
|
||||
checksum=None, # Null checksum
|
||||
content_type="text/markdown",
|
||||
)
|
||||
await entity_service.repository.add(entity)
|
||||
|
||||
@@ -98,6 +101,3 @@ modified: 2024-01-01
|
||||
# Verify entity was properly synced
|
||||
updated = await entity_service.get_by_path_id("concept/incomplete")
|
||||
assert updated.checksum is not None
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user