diff --git a/src/basic_memory/api/routers/discovery_router.py b/src/basic_memory/api/routers/discovery_router.py index 772bca05..e1838948 100644 --- a/src/basic_memory/api/routers/discovery_router.py +++ b/src/basic_memory/api/routers/discovery_router.py @@ -11,12 +11,6 @@ from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedE router = APIRouter(prefix="/discovery", tags=["discovery"]) -@router.get("/entity-types", response_model=EntityTypeList) -async def get_entity_types(entity_service: EntityServiceDep) -> EntityTypeList: - """Get list of all unique entity types in the system.""" - logger.debug("Getting all entity types") - types = await entity_service.get_entity_types() - return EntityTypeList(types=types) @router.get("/observation-categories", response_model=ObservationCategoryList) @@ -26,25 +20,3 @@ async def get_observation_categories(observation_service: ObservationServiceDep) categories = await observation_service.observation_categories() return ObservationCategoryList(categories=categories) - -@router.get("/entities/{entity_type}", response_model=TypedEntityList) -async def list_entities_by_type( - entity_service: EntityServiceDep, - entity_type: str, - include_related: bool = False, - sort_by: Optional[str] = "updated_at", -) -> TypedEntityList: - """List all entities of a specific type.""" - logger.debug(f"Listing entities of type: {entity_type}") - entities = await entity_service.list_entities( - entity_type=entity_type, - sort_by=sort_by, - include_related=include_related - ) - return TypedEntityList( - entity_type=entity_type, - entities=[EntityResponse.model_validate(e) for e in entities], - total=len(entities), - sort_by=sort_by, - include_related=include_related - ) \ No newline at end of file diff --git a/src/basic_memory/deps.py b/src/basic_memory/deps.py index 53a4d6b0..c24fe4dd 100644 --- a/src/basic_memory/deps.py +++ b/src/basic_memory/deps.py @@ -210,6 +210,7 @@ async def get_knowledge_service( relation_service: RelationServiceDep, file_service: FileServiceDep, knowledge_writer: KnowledgeWriterDep, + note_writer: NoteWriterDep, project_config: ProjectConfigDep, ) -> KnowledgeService: """Create KnowledgeService with dependencies.""" @@ -219,6 +220,7 @@ async def get_knowledge_service( relation_service=relation_service, file_service=file_service, knowledge_writer=knowledge_writer, + note_writer=note_writer, base_path=project_config.knowledge_dir, ) diff --git a/src/basic_memory/mcp/tools/__init__.py b/src/basic_memory/mcp/tools/__init__.py index 850b94bd..99f2af8e 100644 --- a/src/basic_memory/mcp/tools/__init__.py +++ b/src/basic_memory/mcp/tools/__init__.py @@ -37,7 +37,6 @@ from basic_memory.mcp.tools.documents import ( ) from basic_memory.mcp.tools.discovery import ( - get_entity_types, get_observation_categories, ) @@ -68,7 +67,6 @@ __all__ = [ "delete_document", # Discovery tools - "get_entity_types", "get_observation_categories", # Activity tools diff --git a/src/basic_memory/mcp/tools/discovery.py b/src/basic_memory/mcp/tools/discovery.py index caf08c9d..1b69625f 100644 --- a/src/basic_memory/mcp/tools/discovery.py +++ b/src/basic_memory/mcp/tools/discovery.py @@ -9,95 +9,6 @@ from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedE from basic_memory.mcp.async_client import client -@mcp.tool( - category="discovery", - description="List all unique entity types in the knowledge graph", - examples=[ - { - "name": "Type Analysis", - "description": "Analyze entity type distribution and patterns", - "code": """ -# Get all entity types -types = await get_entity_types() - -# Analyze distribution by type -type_stats = {} -for entity_type in types["types"]: - entities = await list_by_type( - entity_type=entity_type, - include_related=True - ) - type_stats[entity_type] = { - "count": len(entities.entities), - "with_observations": sum(1 for e in entities.entities - if e.observations), - "with_relations": sum(1 for e in entities.entities - if e.relations) - } - -# Show type analysis -print("Knowledge Graph Structure:") -for type_, stats in type_stats.items(): - print(f"\\n{type_}:") - print(f"- Total: {stats['count']} entities") - print(f"- With observations: {stats['with_observations']}") - print(f"- With relations: {stats['with_relations']}")""" - }, - { - "name": "Custom Type Detection", - "description": "Identify and analyze custom entity types", - "code": """ -# Get all types -types = await get_entity_types() - -# Separate system and custom types -system_types = { - "component", "document", "feature", - "test", "concept" -} -custom_types = [t for t in types["types"] - if t not in system_types] - -if custom_types: - print("Custom entity types discovered:") - for type_ in custom_types: - # Get entities of this type - entities = await list_by_type(type_) - - print(f"\\n{type_} ({len(entities.entities)} entities):") - - # Analyze type characteristics - observations = [o for e in entities.entities - for o in e.observations] - categories = {o.category for o in observations} - relations = [r for e in entities.entities - for r in e.relations] - relation_types = {r.relation_type for r in relations} - - if categories: - print("Used categories:") - for cat in sorted(categories): - print(f"- {cat}") - - if relation_types: - print("\\nRelation types:") - for rt in sorted(relation_types): - print(f"- {rt}")""" - } - ], - output_model=EntityTypeList -) -async def get_entity_types() -> List[str]: - """List all unique entity types in use. - - Returns: - List of unique entity type names used in the knowledge graph - """ - logger.debug("Getting all entity types") - url = "/discovery/entity-types" - response = await client.get(url) - return EntityTypeList.model_validate(response.json()) - @mcp.tool( category="discovery", @@ -180,131 +91,3 @@ async def get_observation_categories() -> List[str]: response = await client.get(url) return ObservationCategoryList.model_validate(response.json()) - -@mcp.tool( - category="discovery", - description="List all entities of a specific type", - examples=[ - { - "name": "Component Analysis", - "description": "Analyze component implementation patterns", - "code": """ -# Get all components with relations -components = await list_by_type( - entity_type="component", - include_related=True -) - -# Analyze implementation patterns -dependency_patterns = defaultdict(list) -implementation_patterns = defaultdict(list) - -for entity in components.entities: - # Analyze dependencies - deps = [r for r in entity.relations - if r.relation_type == "depends_on"] - if deps: - pattern = f"{len(deps)} dependencies" - dependency_patterns[pattern].append(entity.name) - - # Analyze implementation details - tech_obs = [o for o in entity.observations - if o.category == "tech"] - if tech_obs: - pattern = f"{len(tech_obs)} technical notes" - implementation_patterns[pattern].append(entity.name) - -print("Component Implementation Patterns:\\n") -print("Dependency Patterns:") -for pattern, components in dependency_patterns.items(): - print(f"\\n{pattern}:") - for comp in components: - print(f"- {comp}") - -print("\\nImplementation Detail Patterns:") -for pattern, components in implementation_patterns.items(): - print(f"\\n{pattern}:") - for comp in components: - print(f"- {comp}")""" - }, - { - "name": "Feature Coverage", - "description": "Analyze feature implementation status", - "code": """ -# Get all features -features = await list_by_type( - entity_type="feature", - include_related=True, - sort_by="updated_at" -) - -def analyze_feature_status(feature): - # Check implementation - has_component = any( - r.relation_type == "implemented_by" - for r in feature.relations - ) - - # Check testing - has_tests = any( - r.to_id.startswith("test/") - for r in feature.relations - ) - - # Check documentation - has_docs = any( - r.to_id.startswith("document/") - for r in feature.relations - ) - - # Get latest status note - status_notes = [o for o in feature.observations - if o.category == "note"] - latest_status = status_notes[-1].content if status_notes else None - - return { - "implemented": has_component, - "tested": has_tests, - "documented": has_docs, - "status": latest_status - } - -# Show feature coverage -print("Feature Implementation Status:\\n") -for feature in features.entities: - status = analyze_feature_status(feature) - - print(f"{feature.name}:") - print(f"- Implementation: {'✓' if status['implemented'] else '⨯'}") - print(f"- Tests: {'✓' if status['tested'] else '⨯'}") - print(f"- Documentation: {'✓' if status['documented'] else '⨯'}") - if status['status']: - print(f"- Status: {status['status']}") - print()""" - } - ], - output_model=TypedEntityList, -) -async def list_by_type( - entity_type: str, - include_related: bool = False, - sort_by: Optional[str] = "updated_at" -) -> TypedEntityList: - """List all entities of a specific type. - - Args: - entity_type: Type of entities to retrieve - include_related: Whether to include related entities - sort_by: Field to sort results by - - Returns: - TypedEntityList containing matching entities and metadata - """ - logger.debug(f"Listing entities of type: {entity_type}") - params = {"include_related": "true" if include_related else "false"} - if sort_by: - params["sort_by"] = sort_by - - url = f"/discovery/entities/{entity_type}" - response = await client.get(url, params=params) - return TypedEntityList.model_validate(response.json()) \ No newline at end of file diff --git a/tests/api/test_discovery_router.py b/tests/api/test_discovery_router.py index 300bd80a..1dc4e1af 100644 --- a/tests/api/test_discovery_router.py +++ b/tests/api/test_discovery_router.py @@ -4,7 +4,7 @@ import pytest import pytest_asyncio from httpx import AsyncClient -from basic_memory.models.knowledge import Entity, Observation +from basic_memory.models.knowledge import Entity, Observation, EntityType from basic_memory.repository.entity_repository import EntityRepository from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedEntityList @@ -18,7 +18,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entities = [ Entity( name="Memory Service", - entity_type="technical_component", + entity_type=EntityType.KNOWLEDGE, description="Core memory service", path_id="component/memory_service", file_path="component/memory_service.md", @@ -29,7 +29,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: ), Entity( name="File Format", - entity_type="specification", + entity_type=EntityType.KNOWLEDGE, description="File format spec", path_id="spec/file_format", file_path="spec/file_format.md", @@ -40,7 +40,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: ), Entity( name="Technical Decision", - entity_type="decision", + entity_type=EntityType.KNOWLEDGE, description="Architecture decision", path_id="decision/tech_choice", file_path="decision/tech_choice.md", @@ -52,7 +52,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: # Add another technical component for sorting tests Entity( name="API Service", - entity_type="technical_component", + entity_type=EntityType.KNOWLEDGE, description="API layer", path_id="component/api_service", file_path="component/api_service.md", @@ -66,27 +66,6 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: return created -async def test_get_entity_types(client: AsyncClient, test_entities): - """Test getting list of entity types.""" - # 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 "technical_component" in data.types - assert "specification" in data.types - assert "decision" 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)) async def test_get_observation_categories(client: AsyncClient, test_entities): @@ -113,49 +92,4 @@ async def test_get_observation_categories(client: AsyncClient, test_entities): assert len(data.categories) == len(set(data.categories)) -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/technical_component") - assert response.status_code == 200 - - # Parse response - data = TypedEntityList.model_validate(response.json()) - - # Check response structure - assert data.entity_type == "technical_component" - assert len(data.entities) == 2 - assert data.total == 2 - - # Verify content - names = {e.name for e in data.entities} - assert "Memory Service" in names - assert "API Service" in names - -async def test_list_entities_with_sorting(client: AsyncClient, test_entities): - """Test listing entities with different sort options.""" - # Sort by name - response = await client.get("/discovery/entities/technical_component?sort_by=name") - assert response.status_code == 200 - data = TypedEntityList.model_validate(response.json()) - names = [e.name for e in data.entities] - assert names == sorted(names) # Should be alphabetical - - # Sort by path_id - response = await client.get("/discovery/entities/technical_component?sort_by=path_id") - assert response.status_code == 200 - data = TypedEntityList.model_validate(response.json()) - path_ids = [e.path_id for e in data.entities] - assert path_ids == sorted(path_ids) - - -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 - assert data.total == 0 diff --git a/tests/api/test_knowledge_router.py b/tests/api/test_knowledge_router.py index d402d43a..7802e15f 100644 --- a/tests/api/test_knowledge_router.py +++ b/tests/api/test_knowledge_router.py @@ -10,7 +10,7 @@ from basic_memory.schemas import ( EntityResponse, EntityListResponse, ObservationResponse, - RelationResponse, + RelationResponse, EntityType, ) 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": "test", + "entity_type": EntityType.KNOWLEDGE, "observations": ["First observation", "Second observation"], } # Create an entity @@ -56,19 +56,20 @@ async def add_observations(client, path_id: str) -> List[ObservationResponse]: data = response.json() obs_response = EntityResponse.model_validate(data) + assert len(obs_response.observations) == 2 return obs_response.observations async def create_related_entities(client) -> List[RelationResponse]: # pyright: ignore [reportReturnType] # Create two entities to relate entities = [ - {"name": "SourceEntity", "entity_type": "test"}, - {"name": "TargetEntity", "entity_type": "test"}, + {"name": "SourceEntity", "entity_type": EntityType.KNOWLEDGE}, + {"name": "TargetEntity", "entity_type": EntityType.KNOWLEDGE}, ] create_response = await client.post("/knowledge/entities", json={"entities": entities}) created = create_response.json()["entities"] - source_path_id = "test/source_entity" - target_path_id = "test/target_entity" + source_path_id = "source_entity" + target_path_id = "target_entity" # Create relation between them response = await client.post( @@ -113,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": "test"} + data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE} response = await client.post("/knowledge/entities", json={"entities": [data]}) assert response.status_code == 200 data = response.json() @@ -126,8 +127,8 @@ async def test_get_entity(client: AsyncClient): assert response.status_code == 200 entity = response.json() assert entity["name"] == "TestEntity" - assert entity["entity_type"] == "test" - assert entity["path_id"] == "test/test_entity" + assert entity["entity_type"] == EntityType.KNOWLEDGE + assert entity["path_id"] == "test_entity" @pytest.mark.asyncio @@ -140,10 +141,10 @@ 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": "test"} - await client.post("/knowledge/entities", json={"entities": [data]}) + data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE} + response = await client.post("/knowledge/entities", json={"entities": [data]}) - path_id = "test/test_entity" + path_id = "test_entity" # Add observations await add_observations(client, path_id) @@ -153,44 +154,21 @@ async def test_add_observations(client: AsyncClient): assert len(entity["observations"]) == 2 -@pytest.mark.asyncio -async def test_search_nodes(client: AsyncClient): - """Should search for entities in the knowledge graph.""" - # Create a few entities with different names - entities = [ - {"name": "NotFound", "entity_type": "negative"}, - {"name": "AlphaTest", "entity_type": "test"}, - {"name": "BetaTest", "entity_type": "test"}, - {"name": "GammaProduction", "entity_type": "test"}, # match entity_type - ] - await client.post("/knowledge/entities", json={"entities": entities}) - - # Search for "Test" in names - response = await client.post("/knowledge/search", json={"query": "Test"}) - - # Verify search results - assert response.status_code == 200 - data = response.json() - assert data["query"] == "Test" - assert len(data["matches"]) == 3 - names = {entity["name"] for entity in data["matches"]} - assert names == {"AlphaTest", "BetaTest", "GammaProduction"} - @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": "test"}, - {"name": "BetaTest", "entity_type": "test"}, + {"name": "AlphaTest", "entity_type": EntityType.KNOWLEDGE}, + {"name": "BetaTest", "entity_type": EntityType.KNOWLEDGE}, ] await client.post("/knowledge/entities", json={"entities": entities}) # Open nodes by path IDs response = await client.post( "/knowledge/nodes", - json={"path_ids": ["test/alpha_test"]}, + json={"path_ids": ["alpha_test"]}, ) # Verify results @@ -199,15 +177,15 @@ async def test_open_nodes(client: AsyncClient): assert len(data["entities"]) == 1 entity = data["entities"][0] assert entity["name"] == "AlphaTest" - assert entity["entity_type"] == "test" - assert entity["path_id"] == "test/alpha_test" + assert entity["entity_type"] == EntityType.KNOWLEDGE + assert entity["path_id"] == "alpha_test" @pytest.mark.asyncio async def test_delete_entity(client: AsyncClient): """Test DELETE /knowledge/entities with path ID.""" # Create test entity - entity_data = {"name": "TestEntity", "entity_type": "test"} + entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE} await client.post("/knowledge/entities", json={"entities": [entity_data]}) # Test deletion @@ -228,21 +206,21 @@ async def test_delete_entity_bulk(client: AsyncClient): """Test bulk entity deletion using path IDs.""" # Create test entities entities = [ - {"name": "Entity1", "entity_type": "test"}, - {"name": "Entity2", "entity_type": "test"}, + {"name": "Entity1", "entity_type": EntityType.KNOWLEDGE}, + {"name": "Entity2", "entity_type": EntityType.KNOWLEDGE}, ] await client.post("/knowledge/entities", json={"entities": entities}) # Test deletion response = await client.post( - "/knowledge/entities/delete", json={"path_ids": ["test/Entity1", "test/Entity2"]} + "/knowledge/entities/delete", json={"path_ids": ["Entity1", "Entity2"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify entities are gone for name in ["Entity1", "Entity2"]: - path_id = quote(f"test/{name}") + path_id = quote(f"{name}") response = await client.get(f"/knowledge/entities/{path_id}") assert response.status_code == 404 @@ -251,13 +229,13 @@ 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": "test"} + entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - await add_observations(client, "test/TestEntity") + await add_observations(client, "TestEntity") # Delete entity response = await client.post( - "/knowledge/entities/delete", json={"path_ids": ["test/TestEntity"]} + "/knowledge/entities/delete", json={"path_ids": ["TestEntity"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} @@ -271,12 +249,12 @@ 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": "test"} + entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - observations = await add_observations(client, "test/TestEntity") # adds 2 + observations = await add_observations(client, "TestEntity") # adds 2 # Delete specific observations - request_data = {"path_id": "test/TestEntity", "observations": [observations[0].content]} + request_data = {"path_id": "TestEntity", "observations": [observations[0].content]} response = await client.post("/knowledge/observations/delete", json=request_data) assert response.status_code == 200 data = response.json() @@ -316,7 +294,7 @@ async def test_delete_relations(client, relation_repository): 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": ["test/non_existent"]} + "/knowledge/entities/delete", json={"path_ids": ["non_existent"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} @@ -326,10 +304,10 @@ 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": "test"} + entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - request_data = {"path_id": "test/TestEntity", "observations": ["Nonexistent observation"]} + request_data = {"path_id": "TestEntity", "observations": ["Nonexistent observation"]} response = await client.post("/knowledge/observations/delete", json=request_data) assert response.status_code == 200 @@ -344,8 +322,8 @@ async def test_delete_nonexistent_relations(client: AsyncClient): request_data = { "relations": [ { - "from_id": "test/non_existent1", - "to_id": "test/non_existent2", + "from_id": "non_existent1", + "to_id": "non_existent2", "relation_type": "nonexistent", } ] @@ -358,34 +336,21 @@ async def test_delete_nonexistent_relations(client: AsyncClient): assert del_response.entities == [] -# @pytest.mark.asyncio -# async def test_invalid_path_id_format(client: AsyncClient): -# """Test handling of invalid path ID formats.""" -# invalid_path_ids = [ -# "/missing_type/name", -# "type//extra_separator", -# "", -# ] -# for invalid_id in invalid_path_ids: -# path_id = quote(invalid_id) -# response = await client.get(f"/knowledge/entities/{path_id}") -# assert response.status_code == 404 - @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": "test"}, - {"name": "NonEntity", "entity_type": "n_a"}, + {"name": "MainEntity", "entity_type": EntityType.KNOWLEDGE}, + {"name": "NonEntity", "entity_type": EntityType.KNOWLEDGE}, ] await client.post("/knowledge/entities", json={"entities": main_entities}) # 2. Create related entities related_entities = [ - {"name": "RelatedOne", "entity_type": "test"}, - {"name": "RelatedTwo", "entity_type": "test"}, + {"name": "RelatedOne", "entity_type": EntityType.KNOWLEDGE}, + {"name": "RelatedTwo", "entity_type": EntityType.KNOWLEDGE}, ] await client.post("/knowledge/entities", json={"entities": related_entities}) @@ -395,13 +360,13 @@ async def test_full_knowledge_flow(client: AsyncClient): json={ "relations": [ { - "from_id": "test/main_entity", - "to_id": "test/related_one", + "from_id": "main_entity", + "to_id": "related_one", "relation_type": "connects_to", }, { - "from_id": "test/main_entity", - "to_id": "test/related_two", + "from_id": "main_entity", + "to_id": "related_two", "relation_type": "connects_to", }, ] @@ -413,7 +378,7 @@ async def test_full_knowledge_flow(client: AsyncClient): await client.post( "/knowledge/observations", json={ - "path_id": "test/main_entity", + "path_id": "main_entity", "observations": [ {"content": "Connected to first related entity", "category": "tech"}, {"content": "Connected to second related entity", "category": "note"}, @@ -423,7 +388,7 @@ async def test_full_knowledge_flow(client: AsyncClient): ) # 5. Verify full graph structure - path_id = quote("test/MainEntity") + path_id = "MainEntity" main_get = await client.get(f"/knowledge/entities/{path_id}") main_entity = main_get.json() @@ -433,21 +398,21 @@ async def test_full_knowledge_flow(client: AsyncClient): assert len(main_entity["relations"]) == 2 # 6. Search should find all related entities - search = await client.post("/knowledge/search", json={"query": "Related"}) - matches = search.json()["matches"] + search = await client.post("/search/", json={"text": "Related"}) + matches = search.json()["results"] assert ( - len(matches) == 3 - ) # Should find both related entities, and the main one with the observation + len(matches) == 1 + ) # 7. Delete main entity response = await client.post( - "/knowledge/entities/delete", json={"path_ids": ["test/MainEntity", "test/NonEntity"]} + "/knowledge/entities/delete", json={"path_ids": ["MainEntity", "NonEntity"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify deletion - path_id = quote("test/MainEntity") + path_id = "MainEntity" response = await client.get(f"/knowledge/entities/{path_id}") assert response.status_code == 404 @@ -457,7 +422,7 @@ async def test_entity_indexing(client: AsyncClient): """Test entity creation includes search indexing.""" data = { "name": "SearchTest", - "entity_type": "test", + "entity_type": EntityType.KNOWLEDGE, "observations": ["Unique searchable observation"], } @@ -472,7 +437,7 @@ async def test_entity_indexing(client: AsyncClient): assert search_response.status_code == 200 search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 1 - assert search_result.results[0].path_id == "test/search_test" + assert search_result.results[0].path_id == "search_test" assert search_result.results[0].type == SearchItemType.ENTITY.value @@ -482,7 +447,7 @@ async def test_observation_update_indexing(client: AsyncClient): # Create entity data = { "name": "TestEntity", - "entity_type": "test", + "entity_type": EntityType.KNOWLEDGE, "observations": ["Initial observation"], } response = await client.post("/knowledge/entities", json={"entities": [data]}) @@ -512,7 +477,7 @@ async def test_entity_delete_indexing(client: AsyncClient): """Test deleted entities are removed from search index.""" data = { "name": "DeleteTest", - "entity_type": "test", + "entity_type": EntityType.KNOWLEDGE, "observations": ["Searchable observation that should be removed"], } @@ -547,8 +512,8 @@ async def test_relation_indexing(client: AsyncClient): """Test relations are included in search index.""" # Create entities entities = [ - {"name": "SourceTest", "entity_type": "test"}, - {"name": "TargetTest", "entity_type": "test"}, + {"name": "SourceTest", "entity_type": EntityType.KNOWLEDGE}, + {"name": "TargetTest", "entity_type": EntityType.KNOWLEDGE}, ] create_response = await client.post("/knowledge/entities", json={"entities": entities}) assert create_response.status_code == 200 @@ -559,8 +524,8 @@ async def test_relation_indexing(client: AsyncClient): json={ "relations": [ { - "from_id": "test/source_test", - "to_id": "test/target_test", + "from_id": "source_test", + "to_id": "target_test", "relation_type": "sphinx_relation", "context": "Unique sphinx relation context", } @@ -576,4 +541,4 @@ async def test_relation_indexing(client: AsyncClient): search_result = SearchResponse.model_validate(search_response.json()) 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 == {"test/source_test", "test/target_test"} + assert path_ids == {"source_test", "target_test"} diff --git a/tests/api/test_search_router.py b/tests/api/test_search_router.py index 346e1942..7d8c74d3 100644 --- a/tests/api/test_search_router.py +++ b/tests/api/test_search_router.py @@ -6,6 +6,7 @@ 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 @@ -15,7 +16,8 @@ def test_entity(): class Entity: id = 1 name = "TestComponent" - entity_type = "component" + entity_type = EntityType.KNOWLEDGE + entity_metadata = { "test": "test"} path_id = "component/test_component" file_path = "entities/component/test_component.md" description = "A test component for search testing" @@ -108,7 +110,7 @@ async def test_search_with_entity_type_filter(client, indexed_entity): "/search/", json={ "text": "test", - "entity_types": ["component"] + "entity_types": [EntityType.KNOWLEDGE] } ) assert response.status_code == 200 @@ -120,7 +122,7 @@ async def test_search_with_entity_type_filter(client, indexed_entity): "/search/", json={ "text": "test", - "entity_types": ["concept"] + "entity_types": [EntityType.NOTE] } ) assert response.status_code == 200 @@ -251,7 +253,7 @@ async def test_multiple_filters(client, indexed_entity): json={ "text": "test", "types": [SearchItemType.ENTITY.value], - "entity_types": ["component"], + "entity_types": [EntityType.KNOWLEDGE], "after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat() } ) @@ -261,4 +263,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"] == "component" \ No newline at end of file + assert result.metadata["entity_type"] == EntityType.KNOWLEDGE \ No newline at end of file diff --git a/tests/mcp/test_tool_activity.py b/tests/mcp/test_tool_activity.py index dbc8e44b..017db173 100644 --- a/tests/mcp/test_tool_activity.py +++ b/tests/mcp/test_tool_activity.py @@ -5,7 +5,7 @@ import pytest from basic_memory.mcp.tools.documents import create_document from basic_memory.mcp.tools.knowledge import create_entities from basic_memory.mcp.tools.activity import get_recent_activity -from basic_memory.schemas.base import Entity +from basic_memory.schemas.base import Entity, EntityType from basic_memory.schemas.request import CreateEntityRequest, DocumentRequest from basic_memory.schemas.activity import ActivityType @@ -61,7 +61,7 @@ async def test_get_recent_activity_filtered(client): ) entity_request = CreateEntityRequest( - entities=[Entity(name="TestEntity", entity_type="test")] + entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)] ) entity = await create_entities(entity_request) @@ -94,7 +94,7 @@ async def test_get_recent_activity_content_handling(client): entity_request = CreateEntityRequest( entities=[Entity( name="ContentEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="Entity description" )] ) @@ -130,8 +130,8 @@ async def test_get_recent_activity_multiple_types(client): entity_request = CreateEntityRequest( entities=[ - Entity(name="Entity1", entity_type="test"), - Entity(name="Entity2", entity_type="test") + Entity(name="Entity1", entity_type=EntityType.KNOWLEDGE), + Entity(name="Entity2", entity_type=EntityType.KNOWLEDGE) ] ) entities = await create_entities(entity_request) diff --git a/tests/mcp/test_tool_add_observations.py b/tests/mcp/test_tool_add_observations.py index 18b01b8e..5b9a2c08 100644 --- a/tests/mcp/test_tool_add_observations.py +++ b/tests/mcp/test_tool_add_observations.py @@ -3,7 +3,7 @@ import pytest from basic_memory.mcp.tools.knowledge import create_entities, add_observations -from basic_memory.schemas.base import ObservationCategory, Entity +from basic_memory.schemas.base import ObservationCategory, Entity, EntityType from basic_memory.schemas.request import CreateEntityRequest, AddObservationsRequest, ObservationCreate @@ -12,7 +12,7 @@ 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="test")] + entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)] ) result = await create_entities(entity_request) entity_id = result.entities[0].path_id @@ -38,7 +38,7 @@ 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="test")] + entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)] ) result = await create_entities(entity_request) entity_id = result.entities[0].path_id @@ -80,7 +80,7 @@ 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="test")] + entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)] ) result = await create_entities(entity_request) entity_id = result.entities[0].path_id @@ -118,7 +118,7 @@ async def test_add_observations_preserves_existing(client): entities=[ Entity( name="TestEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, observations=["Initial observation"] ) ] @@ -150,7 +150,7 @@ 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="test")] + entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)] ) result = await create_entities(entity_request) entity_id = result.entities[0].path_id diff --git a/tests/mcp/test_tool_create_entities.py b/tests/mcp/test_tool_create_entities.py index 80fa6586..295cbfac 100644 --- a/tests/mcp/test_tool_create_entities.py +++ b/tests/mcp/test_tool_create_entities.py @@ -3,7 +3,7 @@ import pytest from basic_memory.mcp.tools.knowledge import create_entities -from basic_memory.schemas.base import ObservationCategory, Entity +from basic_memory.schemas.base import ObservationCategory, Entity, EntityType from basic_memory.schemas.request import CreateEntityRequest @@ -14,7 +14,7 @@ async def test_create_basic_entity(client): entities=[ Entity( name="TestEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="A test entity", observations=["First observation"] ) @@ -29,8 +29,8 @@ async def test_create_basic_entity(client): # Check the created entity entity = result.entities[0] assert entity.name == "TestEntity" - assert entity.entity_type == "test" - assert entity.path_id == "test/test_entity" + assert entity.entity_type == EntityType.KNOWLEDGE + assert entity.path_id == "test_entity" assert entity.description == "A test entity" # Check observations @@ -50,7 +50,7 @@ async def test_create_entity_with_multiple_observations(client): entities=[ Entity( name="TestEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="A test entity", observations=[ "First observation", @@ -85,12 +85,12 @@ async def test_create_multiple_entities(client): entities=[ Entity( name="Entity1", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, observations=["Observation 1"] ), Entity( name="Entity2", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, observations=["Observation 2"] ) ] @@ -115,7 +115,7 @@ async def test_create_entity_without_observations(client): entities=[ Entity( name="TestEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="A test entity without observations" ) ] @@ -135,7 +135,7 @@ async def test_create_minimal_entity(client): entities=[ Entity( name="MinimalEntity", - entity_type="test" + entity_type=EntityType.KNOWLEDGE ) ] ) @@ -144,8 +144,8 @@ async def test_create_minimal_entity(client): entity = result.entities[0] assert entity.name == "MinimalEntity" - assert entity.entity_type == "test" - assert entity.path_id == "test/minimal_entity" + assert entity.entity_type == EntityType.KNOWLEDGE + assert entity.path_id == "minimal_entity" assert entity.description is None assert len(entity.observations) == 0 assert len(entity.relations) == 0 \ No newline at end of file diff --git a/tests/mcp/test_tool_create_relations.py b/tests/mcp/test_tool_create_relations.py index f3221c2d..0eaf2899 100644 --- a/tests/mcp/test_tool_create_relations.py +++ b/tests/mcp/test_tool_create_relations.py @@ -3,7 +3,7 @@ import pytest from basic_memory.mcp.tools.knowledge import create_entities, create_relations -from basic_memory.schemas.base import Relation, Entity +from basic_memory.schemas.base import Relation, Entity, EntityType from basic_memory.schemas.request import CreateEntityRequest, CreateRelationsRequest from basic_memory.services.exceptions import EntityNotFoundError @@ -14,8 +14,8 @@ async def test_create_basic_relation(client): # First create test entities entity_request = CreateEntityRequest( entities=[ - Entity(name="SourceEntity", entity_type="test"), - Entity(name="TargetEntity", entity_type="test") + Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE), + Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) @@ -24,8 +24,8 @@ async def test_create_basic_relation(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_id="test/source_entity", - to_id="test/target_entity", + from_id="source_entity", + to_id="target_entity", relation_type="depends_on" ) ] @@ -35,8 +35,8 @@ async def test_create_basic_relation(client): assert len(result.entities) == 2 # Find source and target entities - source = next(e for e in result.entities if e.path_id == "test/source_entity") - target = next(e for e in result.entities if e.path_id == "test/target_entity") + 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 @@ -44,14 +44,14 @@ async def test_create_basic_relation(client): # Source's relation shows it depends_on target source_relation = source.relations[0] - assert source_relation.from_id == "test/source_entity" - assert source_relation.to_id == "test/target_entity" + assert source_relation.from_id == "source_entity" + assert source_relation.to_id == "target_entity" assert source_relation.relation_type == "depends_on" # Target's relation is the same, allowing backwards traversal target_relation = target.relations[0] - assert target_relation.from_id == "test/source_entity" - assert target_relation.to_id == "test/target_entity" + assert target_relation.from_id == "source_entity" + assert target_relation.to_id == "target_entity" assert target_relation.relation_type == "depends_on" @@ -61,8 +61,8 @@ async def test_create_relation_with_context(client): # Create test entities entity_request = CreateEntityRequest( entities=[ - Entity(name="Source", entity_type="test"), - Entity(name="Target", entity_type="test") + Entity(name="Source", entity_type=EntityType.KNOWLEDGE), + Entity(name="Target", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) @@ -70,8 +70,8 @@ async def test_create_relation_with_context(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_id="test/source", - to_id="test/target", + from_id="source", + to_id="target", relation_type="implements", context="Implementation details" ) @@ -79,8 +79,8 @@ async def test_create_relation_with_context(client): ) result = await create_relations(relation_request) - source = next(e for e in result.entities if e.path_id == "test/source") - target = next(e for e in result.entities if e.path_id == "test/target") + source = next(e for e in result.entities if e.path_id == "source") + target = next(e for e in result.entities if e.path_id == "target") # Both entities should have the relation with context assert len(source.relations) == 1 @@ -95,9 +95,9 @@ async def test_create_multiple_relations(client): # Create test entities entity_request = CreateEntityRequest( entities=[ - Entity(name="Entity1", entity_type="test"), - Entity(name="Entity2", entity_type="test"), - Entity(name="Entity3", entity_type="test") + Entity(name="Entity1", entity_type=EntityType.KNOWLEDGE), + Entity(name="Entity2", entity_type=EntityType.KNOWLEDGE), + Entity(name="Entity3", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) @@ -105,13 +105,13 @@ async def test_create_multiple_relations(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_id="test/entity1", - to_id="test/entity2", + from_id="entity1", + to_id="entity2", relation_type="connects_to" ), Relation( - from_id="test/entity2", - to_id="test/entity3", + from_id="entity2", + to_id="entity3", relation_type="depends_on" ) ] @@ -122,9 +122,9 @@ async def test_create_multiple_relations(client): assert len(result.entities) == 3 # Get entities - entity1 = next(e for e in result.entities if e.path_id == "test/entity1") - entity2 = next(e for e in result.entities if e.path_id == "test/entity2") - entity3 = next(e for e in result.entities if e.path_id == "test/entity3") + 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 @@ -144,8 +144,8 @@ async def test_create_bidirectional_relations(client): # Create test entities entity_request = CreateEntityRequest( entities=[ - Entity(name="Service", entity_type="test"), - Entity(name="Database", entity_type="test") + Entity(name="Service", entity_type=EntityType.KNOWLEDGE), + Entity(name="Database", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) @@ -154,21 +154,21 @@ async def test_create_bidirectional_relations(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_id="test/service", - to_id="test/database", + from_id="service", + to_id="database", relation_type="depends_on" ), Relation( - from_id="test/database", - to_id="test/service", + from_id="database", + to_id="service", relation_type="supports" ) ] ) result = await create_relations(relation_request) - service = next(e for e in result.entities if e.path_id == "test/service") - database = next(e for e in result.entities if e.path_id == "test/database") + service = next(e for e in result.entities if e.path_id == "service") + database = next(e for e in result.entities if e.path_id == "database") # Each entity should have both relations for full navigation assert len(service.relations) == 2 @@ -189,7 +189,7 @@ async def test_create_relation_with_invalid_entity(client): # Create only one of the needed entities entity_request = CreateEntityRequest( entities=[ - Entity(name="RealEntity", entity_type="test") + Entity(name="RealEntity", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) @@ -197,8 +197,8 @@ async def test_create_relation_with_invalid_entity(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_id="test/real_entity", - to_id="test/non_existent_entity", + from_id="real_entity", + to_id="non_existent_entity", relation_type="depends_on" ) ] @@ -215,16 +215,16 @@ async def test_create_duplicate_relation(client): # Create test entities entity_request = CreateEntityRequest( entities=[ - Entity(name="Source", entity_type="test"), - Entity(name="Target", entity_type="test") + Entity(name="Source", entity_type=EntityType.KNOWLEDGE), + Entity(name="Target", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) # Create relation relation = Relation( - from_id="test/source", - to_id="test/target", + from_id="source", + to_id="target", relation_type="connects_to" ) relation_request = CreateRelationsRequest(relations=[relation]) diff --git a/tests/mcp/test_tool_discovery.py b/tests/mcp/test_tool_discovery.py index 9fcdd0ba..5b2789fe 100644 --- a/tests/mcp/test_tool_discovery.py +++ b/tests/mcp/test_tool_discovery.py @@ -3,67 +3,13 @@ import pytest from basic_memory.mcp.tools.discovery import ( - get_entity_types, get_observation_categories, - list_by_type, ) -from basic_memory.schemas import Entity, CreateEntityRequest, EntityTypeList, ObservationCategoryList +from basic_memory.schemas import Entity, CreateEntityRequest, ObservationCategoryList, EntityType from basic_memory.mcp.tools.knowledge import create_entities, add_observations from basic_memory.schemas.request import ObservationCreate, AddObservationsRequest -@pytest.mark.asyncio -async def test_get_entity_types(client): - """Test getting list of entity types.""" - # First create some test entities - request = CreateEntityRequest( - entities=[ - Entity( - name="Memory Service", - entity_type="technical_component", - path_id="component/memory_service", - description="Core memory service", - observations=["Using SQLite for storage", "Local-first architecture"] - ), - Entity( - name="File Format", - entity_type="specification", - path_id="specification/file_format", - description="File format spec", - observations=["Support for frontmatter", "UTF-8 encoding"] - ), - Entity( - name="Tech Choice", - entity_type="decision", - path_id="decision/tech_choice", - description="Technology decision", - observations=["Team discussed options", "Selected for scalability"] - ) - ] - ) - await create_entities(request) - - # Get entity types - result = await get_entity_types() - entity_types = EntityTypeList.model_validate(result) - - # Verify the result - assert "technical_component" in entity_types.types - assert "specification" in entity_types.types - assert "decision" in entity_types.types - - -@pytest.mark.asyncio -async def test_get_entity_types_empty(client): - """Test getting entity types when no entities exist.""" - # Get types (should work with empty DB) - result = await get_entity_types() - entity_types = EntityTypeList.model_validate(result) - - # Should return empty list, not error - assert len(entity_types.types) == 0 - - @pytest.mark.asyncio async def test_get_observation_categories(client): """Test getting list of observation categories.""" @@ -72,7 +18,7 @@ async def test_get_observation_categories(client): entities=[ Entity( name="Test Entity", - entity_type="test_observation", + entity_type=EntityType.KNOWLEDGE, description="Test entity", observations=[], ) @@ -95,7 +41,7 @@ async def test_get_observation_categories(client): # Get categories result = await get_observation_categories() observation_categories = ObservationCategoryList.model_validate(result) - + # Verify results assert "tech" in observation_categories.categories assert "design" in observation_categories.categories @@ -103,36 +49,11 @@ async def test_get_observation_categories(client): assert "note" in observation_categories.categories - -@pytest.mark.asyncio -async def test_list_by_type_with_sorting(client): - """Test listing entities with different sort options.""" - # Sort by name - result = await list_by_type("technical_component", sort_by="name") - names = [e.name for e in result.entities] - assert names == sorted(names) - - # Sort by path_id - result = await list_by_type("technical_component", sort_by="path_id") - path_ids = [e.path_id for e in result.entities] - assert path_ids == sorted(path_ids) - - -@pytest.mark.asyncio -async def test_list_by_type_empty(client): - """Test listing entities for a type that doesn't exist.""" - result = await list_by_type("nonexistent_type") - - assert result.entity_type == "nonexistent_type" - assert len(result.entities) == 0 - assert result.total == 0 - - @pytest.mark.asyncio async def test_get_observation_categories_empty(client): """Test getting observation categories when no observations exist.""" result = await get_observation_categories() observation_categories = ObservationCategoryList.model_validate(result) - + # Should return empty list, not error - observation_categories.categories == [] \ No newline at end of file + observation_categories.categories == [] diff --git a/tests/mcp/test_tool_get_entity.py b/tests/mcp/test_tool_get_entity.py index 34c69448..70f07cd3 100644 --- a/tests/mcp/test_tool_get_entity.py +++ b/tests/mcp/test_tool_get_entity.py @@ -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 +from basic_memory.schemas.base import Entity, ObservationCategory, EntityType from basic_memory.schemas.request import CreateEntityRequest from basic_memory.services.exceptions import EntityNotFoundError @@ -16,7 +16,7 @@ async def test_get_basic_entity(client): entities=[ Entity( name="TestEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="A test entity", observations=["First observation"] ) @@ -30,8 +30,8 @@ async def test_get_basic_entity(client): # Verify entity details assert entity.name == "TestEntity" - assert entity.entity_type == "test" - assert entity.path_id == "test/test_entity" + assert entity.entity_type == EntityType.KNOWLEDGE + assert entity.path_id == "test_entity" assert entity.description == "A test entity" # Check observations @@ -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="test"), - Entity(name="TargetEntity", entity_type="test") + Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE), + Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE) ] ) await create_entities(entity_request) @@ -61,8 +61,8 @@ async def test_get_entity_with_relations(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_id="test/source_entity", - to_id="test/target_entity", + from_id="source_entity", + to_id="target_entity", relation_type="depends_on" ) ] @@ -70,10 +70,10 @@ async def test_get_entity_with_relations(client): await create_relations(relation_request) # Get and verify source entity - source = await get_entity("test/source_entity") + source = await get_entity("source_entity") assert len(source.relations) == 1 relation = source.relations[0] - assert relation.to_id == "test/target_entity" + assert relation.to_id == "target_entity" assert relation.relation_type == "depends_on" @@ -85,7 +85,7 @@ async def test_get_entity_with_categorized_observations(client): entities=[ Entity( name="TestEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity with categories" ) ] diff --git a/tests/mcp/test_tool_open_nodes.py b/tests/mcp/test_tool_open_nodes.py index 3b864c90..33fb2116 100644 --- a/tests/mcp/test_tool_open_nodes.py +++ b/tests/mcp/test_tool_open_nodes.py @@ -5,7 +5,7 @@ import pytest from basic_memory.mcp.tools.search import open_nodes from basic_memory.mcp.tools.knowledge import create_entities from basic_memory.schemas import EntityListResponse -from basic_memory.schemas.base import Entity +from basic_memory.schemas.base import Entity, EntityType from basic_memory.schemas.request import CreateEntityRequest, OpenNodesRequest @@ -17,12 +17,12 @@ async def test_open_multiple_entities(client): entities=[ Entity( name="Entity1", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="First test entity" ), Entity( name="Entity2", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="Second test entity" ) ] @@ -52,7 +52,7 @@ async def test_open_nodes_with_details(client): entities=[ Entity( name="DetailedEntity", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity with details", observations=["First observation", "Second observation"] ) @@ -69,7 +69,7 @@ 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 == "test" + assert entity.entity_type == EntityType.KNOWLEDGE assert entity.description == "Test entity with details" assert len(entity.observations) == 2 @@ -82,12 +82,12 @@ async def test_open_nodes_with_relations(client): entities=[ Entity( name="Service", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="A service" ), Entity( name="Database", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, description="A database" ) ] @@ -129,7 +129,7 @@ async def test_open_nonexistent_nodes(client): entities=[ Entity( name="RealEntity", - entity_type="test" + entity_type=EntityType.KNOWLEDGE ) ] ) @@ -138,7 +138,7 @@ async def test_open_nonexistent_nodes(client): # Try to open both real and non-existent request = OpenNodesRequest( - path_ids=[real_path_id, "test/nonexistent"] + path_ids=[real_path_id, "nonexistent"] ) result = await open_nodes(request) response = EntityListResponse.model_validate(result) @@ -156,7 +156,7 @@ async def test_open_single_node(client): entities=[ Entity( name="SingleEntity", - entity_type="test" + entity_type=EntityType.KNOWLEDGE ) ] ) diff --git a/tests/repository/test_observation_repository.py b/tests/repository/test_observation_repository.py index 9c5de9e3..29be93dc 100644 --- a/tests/repository/test_observation_repository.py +++ b/tests/repository/test_observation_repository.py @@ -7,6 +7,7 @@ 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 @@ -89,7 +90,7 @@ 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="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity", path_id="test/test_entity", file_path="test/test_entity.md", @@ -118,7 +119,7 @@ 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="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity", path_id="test/test_entity", file_path="test/test_entity.md", @@ -146,7 +147,7 @@ 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="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity", path_id="test/test_entity", file_path="test/test_entity.md", @@ -176,7 +177,7 @@ 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="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity", path_id="test/test_entity", file_path="test/test_entity.md", @@ -217,7 +218,7 @@ 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="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity", path_id="test/test_entity", file_path="test/test_entity.md", @@ -264,7 +265,7 @@ 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="test", + entity_type=EntityType.KNOWLEDGE, description="Test entity", path_id="test/test_entity", file_path="test/test_entity.md", diff --git a/tests/repository/test_relation_repository.py b/tests/repository/test_relation_repository.py index 261d97e8..bff0f8ed 100644 --- a/tests/repository/test_relation_repository.py +++ b/tests/repository/test_relation_repository.py @@ -6,6 +6,7 @@ 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 @@ -14,7 +15,7 @@ async def source_entity(session_maker): """Create a source entity for testing relations.""" entity = Entity( name="test_source", - entity_type="source", + entity_type=EntityType.KNOWLEDGE, path_id="source/test_source", file_path="source/test_source.md", description="Source entity", @@ -30,7 +31,7 @@ async def target_entity(session_maker): """Create a target entity for testing relations.""" entity = Entity( name="test_target", - entity_type="target", + entity_type=EntityType.KNOWLEDGE, path_id="target/test_target", file_path="target/test_target.md", description="Target entity", @@ -59,7 +60,7 @@ async def related_entity(entity_repository): """Create a second entity for testing relations""" entity_data = { "name": "Related Entity", - "entity_type": "test", + "entity_type": EntityType.KNOWLEDGE, "path_id": "test/related_entity", "file_path": "test/related_entity.md", "description": "A related test entity", diff --git a/tests/schemas/test_schemas.py b/tests/schemas/test_schemas.py index 5bd2c838..2b4f41ea 100644 --- a/tests/schemas/test_schemas.py +++ b/tests/schemas/test_schemas.py @@ -16,10 +16,10 @@ from basic_memory.schemas.base import to_snake_case def test_entity_in_minimal(): """Test creating EntityIn with minimal required fields.""" - data = {"name": "test_entity", "entity_type": "test"} + data = {"name": "test_entity", "entity_type": "knowledge"} entity = Entity.model_validate(data) assert entity.name == "test_entity" - assert entity.entity_type == "test" + assert entity.entity_type == "knowledge" assert entity.description is None assert entity.observations == [] @@ -28,13 +28,13 @@ def test_entity_in_complete(): """Test creating EntityIn with all fields.""" data = { "name": "test_entity", - "entity_type": "test", + "entity_type": "knowledge", "description": "A test entity", "observations": ["Test observation"], } entity = Entity.model_validate(data) assert entity.name == "test_entity" - assert entity.entity_type == "test" + assert entity.entity_type == "knowledge" assert entity.description == "A test entity" assert len(entity.observations) == 1 assert entity.observations[0] == "Test observation" @@ -84,8 +84,8 @@ def test_create_entities_input(): """Test CreateEntitiesInput validation.""" data = { "entities": [ - {"name": "entity1", "entity_type": "test"}, - {"name": "entity2", "entity_type": "test", "description": "test description"}, + {"name": "entity1", "entity_type": "knowledge"}, + {"name": "entity2", "entity_type": "knowledge", "description": "test description"}, ] } create_input = CreateEntityRequest.model_validate(data) @@ -103,7 +103,7 @@ def test_entity_out_from_attributes(): db_data = { "path_id": "test/test", "name": "test", - "entity_type": "test", + "entity_type": "knowledge", "description": "test description", "observations": [{"id": 1, "content": "test obs", "context": None}], "relations": [ @@ -120,7 +120,7 @@ def test_entity_out_from_attributes(): def test_optional_fields(): """Test handling of optional fields.""" # Create with no optional fields - entity = Entity.model_validate({"name": "test", "entity_type": "test"}) + entity = Entity.model_validate({"name": "test", "entity_type": "knowledge"}) assert entity.description is None assert entity.observations == [] @@ -128,7 +128,7 @@ def test_optional_fields(): entity = Entity.model_validate( { "name": "test", - "entity_type": "test", + "entity_type": "knowledge", "description": None, "observations": [], } @@ -138,7 +138,7 @@ def test_optional_fields(): # Create with some optional fields entity = Entity.model_validate( - {"name": "test", "entity_type": "test", "description": "test", "observations": []} + {"name": "test", "entity_type": "knowledge", "description": "test", "observations": []} ) assert entity.description == "test" assert entity.observations == [] @@ -187,20 +187,20 @@ def test_path_id_generation(): """Test path_id property generates correct paths.""" test_cases = [ ( - {"name": "BasicMemory", "entity_type": "Project"}, - "project/basic_memory" + {"name": "BasicMemory", "entity_type": "knowledge"}, + "basic_memory" ), ( - {"name": "Memory Service", "entity_type": "Component"}, - "component/memory_service" + {"name": "Memory Service", "entity_type": "knowledge"}, + "memory_service" ), ( - {"name": "API Gateway", "entity_type": "Service"}, - "service/api_gateway" + {"name": "API Gateway", "entity_type": "knowledge"}, + "api_gateway" ), ( - {"name": "TestCase1", "entity_type": "Test"}, - "test/test_case1" + {"name": "TestCase1", "entity_type": "knowledge"}, + "test_case1" ), ] @@ -208,33 +208,3 @@ def test_path_id_generation(): entity = Entity.model_validate(input_data) assert entity.path_id == expected_path, f"Failed for input: {input_data}" - -# def test_path_id_validation(): -# """Test path ID format validation.""" -# valid_paths = [ -# "project/basic_memory", -# "test/test_case_1", -# "component/api_gateway", -# ] -# -# invalid_paths = [ -# "no_separator", # Missing / -# "/missing_type", # Missing type -# "type/", # Missing name -# "type//double", # Double separator -# "../path/traversal", # Path traversal attempt -# "type/name/extra", # Too many parts -# "", # Empty string -# ] -# -# # Test valid paths -# for path in valid_paths: -# try: -# Relation.model_validate({"from_id": path, "to_id": path, "relation_type": "test"}) -# except ValidationError as e: -# assert False, f"Valid path {path} failed validation: {e}" -# -# # Test invalid paths -# for path in invalid_paths: -# with pytest.raises(ValidationError): -# Relation.model_validate({"from_id": path, "to_id": "test/valid", "relation_type": "test"}) \ No newline at end of file diff --git a/tests/services/test_relation_service.py b/tests/services/test_relation_service.py index 868905e6..030517ce 100644 --- a/tests/services/test_relation_service.py +++ b/tests/services/test_relation_service.py @@ -5,6 +5,7 @@ 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 @@ -18,14 +19,14 @@ async def test_entities( async with session_maker() as session: entity1 = Entity( name="test_entity_1", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, path_id="test/test_entity_1", file_path="test/test_entity_1.md", description="Test entity 1", ) entity2 = Entity( name="test_entity_2", - entity_type="test", + entity_type=EntityType.KNOWLEDGE, path_id="test/test_entity_2", file_path="test/test_entity_2.md", description="Test entity 2", diff --git a/tests/services/test_search_service.py b/tests/services/test_search_service.py index f79b8aa6..de26c640 100644 --- a/tests/services/test_search_service.py +++ b/tests/services/test_search_service.py @@ -15,7 +15,8 @@ def test_entity(): class Entity: id = 1 name = "TestComponent" - entity_type = "component" + entity_type = "knowledge" + entity_metadata = { "test": "test"} path_id = "component/test_component" file_path = "entities/component/test_component.md" description = "A test component for search" @@ -75,7 +76,7 @@ async def test_search_filtering(search_service, test_entity): SearchQuery( text="test", types=[SearchItemType.ENTITY], - entity_types=["component"] + entity_types=["knowledge"] ) ) assert len(results) == 1 diff --git a/tests/sync/test_knowledge_sync_service.py b/tests/sync/test_knowledge_sync_service.py index 4be4f405..b7b1cd0f 100644 --- a/tests/sync/test_knowledge_sync_service.py +++ b/tests/sync/test_knowledge_sync_service.py @@ -14,6 +14,7 @@ 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 @@ -21,7 +22,7 @@ from basic_memory.sync.knowledge_sync_service import KnowledgeSyncService def test_frontmatter() -> EntityFrontmatter: """Create test frontmatter.""" return EntityFrontmatter( - type="concept", + type="knowledge", id="concept/test_entity", created=datetime.now(), modified=datetime.now(), @@ -64,7 +65,7 @@ async def test_create_entity_without_relations( # Check basic fields assert entity.name == "Test Entity" - assert entity.entity_type == "concept" + assert entity.entity_type == EntityType.KNOWLEDGE assert entity.path_id == "concept/test_entity" assert entity.description == "A test entity description" @@ -119,13 +120,13 @@ async def test_update_entity_relations( # Create target entities that relations point to other_entity = EntityModel( name="Other Entity", - entity_type="concept", + entity_type=EntityType.KNOWLEDGE, path_id="concept/other_entity", file_path="concept/other_entity.md", ) another_entity = EntityModel( name="Another Entity", - entity_type="concept", + entity_type=EntityType.KNOWLEDGE, path_id="concept/another_entity", file_path="concept/another_entity.md", ) @@ -162,13 +163,13 @@ async def test_two_pass_sync_flow( # Create target entities first other_entity = EntityModel( name="Other Entity", - entity_type="concept", + entity_type=EntityType.KNOWLEDGE, path_id="concept/other_entity", file_path="concept/other_entity.md", ) another_entity = EntityModel( name="Another Entity", - entity_type="concept", + entity_type=EntityType.KNOWLEDGE, path_id="concept/another_entity", file_path="concept/another_entity.md", ) diff --git a/tests/sync/test_sync_knowledge.py b/tests/sync/test_sync_knowledge.py index 497ce343..cd720fd0 100644 --- a/tests/sync/test_sync_knowledge.py +++ b/tests/sync/test_sync_knowledge.py @@ -6,6 +6,7 @@ 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 @@ -27,7 +28,7 @@ async def test_sync_knowledge( # New entity with relation new_content = """ --- -type: concept +type: knowledge id: concept/test_concept created: 2023-01-01 modified: 2023-01-01 @@ -48,7 +49,7 @@ A test concept. other = Entity( path_id="concept/other", name="Other", - entity_type="concept", + entity_type=EntityType.KNOWLEDGE, file_path="concept/other.md", checksum="12345678", ) @@ -63,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 == "concept" + assert test_concept.entity_type == EntityType.KNOWLEDGE # Verify relation was not created # because file for related entity was not found @@ -82,7 +83,7 @@ async def test_sync_entity_with_nonexistent_relations( # Create entity that references entities we haven't created yet content = """ --- -type: concept +type: knowledge id: concept/depends_on_future created: 2024-01-01 modified: 2024-01-01 @@ -119,7 +120,7 @@ async def test_sync_entity_circular_relations( # Create entity A that depends on B content_a = """ --- -type: concept +type: knowledge id: concept/entity_a created: 2024-01-01 modified: 2024-01-01 @@ -137,7 +138,7 @@ modified: 2024-01-01 # Create entity B that depends on A content_b = """ --- -type: concept +type: knowledge id: concept/entity_b created: 2024-01-01 modified: 2024-01-01 @@ -193,7 +194,7 @@ async def test_sync_entity_duplicate_relations( # Create target entity first target_content = """ --- -type: concept +type: knowledge id: concept/target created: 2024-01-01 modified: 2024-01-01 @@ -209,7 +210,7 @@ modified: 2024-01-01 # Create entity with duplicate relations content = """ --- -type: concept +type: knowledge id: concept/duplicate_relations created: 2024-01-01 modified: 2024-01-01 @@ -254,7 +255,7 @@ async def test_sync_entity_with_invalid_category( content = """ --- -type: concept +type: knowledge id: concept/invalid_category created: 2024-01-01 modified: 2024-01-01 @@ -297,7 +298,7 @@ async def test_sync_entity_with_order_dependent_relations( entities = { "a": """ --- -type: concept +type: knowledge id: concept/entity_a created: 2024-01-01 modified: 2024-01-01 @@ -314,7 +315,7 @@ modified: 2024-01-01 """, "b": """ --- -type: concept +type: knowledge id: concept/entity_b created: 2024-01-01 modified: 2024-01-01 @@ -329,7 +330,7 @@ modified: 2024-01-01 """, "c": """ --- -type: concept +type: knowledge id: concept/entity_c created: 2024-01-01 modified: 2024-01-01 diff --git a/tests/sync/test_sync_service.py b/tests/sync/test_sync_service.py index d167aa9d..b64daa1a 100644 --- a/tests/sync/test_sync_service.py +++ b/tests/sync/test_sync_service.py @@ -7,6 +7,7 @@ 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 @@ -60,7 +61,7 @@ async def test_sync_null_checksum_cleanup( entity = Entity( path_id="concept/incomplete", name="Incomplete", - entity_type="concept", + entity_type=EntityType.KNOWLEDGE, file_path="concept/incomplete.md", checksum=None, # Null checksum ) @@ -69,7 +70,7 @@ async def test_sync_null_checksum_cleanup( # Create corresponding file content = """ --- -type: concept +type: knowledge id: concept/incomplete created: 2024-01-01 modified: 2024-01-01 @@ -99,7 +100,7 @@ async def test_sync_mixed_document_types(sync_service: SyncService, test_config: # Create a knowledge file knowledge_content = """ --- -type: concept +type: knowledge id: concept/test created: 2024-01-01 modified: 2024-01-01 @@ -134,7 +135,7 @@ async def test_sync_performance_large_files(sync_service: SyncService, test_conf observations = [f"- Observation {i}" for i in range(100)] knowledge_content = f""" --- -type: concept +type: knowledge id: concept/large created: 2024-01-01 modified: 2024-01-01