mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
get tests working
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"
|
||||
assert result.metadata["entity_type"] == EntityType.KNOWLEDGE
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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])
|
||||
|
||||
@@ -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 == []
|
||||
observation_categories.categories == []
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"})
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user