diff --git a/src/basic_memory/cli/commands/sync.py b/src/basic_memory/cli/commands/sync.py index 43925836..17063298 100644 --- a/src/basic_memory/cli/commands/sync.py +++ b/src/basic_memory/cli/commands/sync.py @@ -18,7 +18,7 @@ from basic_memory import db from basic_memory.cli.app import app from basic_memory.config import config from basic_memory.db import DatabaseType -from basic_memory.markdown import KnowledgeParser +from basic_memory.markdown import EntityParser from basic_memory.repository import ( EntityRepository, ObservationRepository, @@ -27,8 +27,6 @@ from basic_memory.repository import ( from basic_memory.repository.search_repository import SearchRepository from basic_memory.services import ( EntityService, - ObservationService, - RelationService, ) from basic_memory.services.search_service import SearchService from basic_memory.sync import SyncService, FileChangeScanner, KnowledgeSyncService @@ -61,13 +59,11 @@ async def get_sync_service(db_type=DatabaseType.FILESYSTEM): # Initialize services entity_service = EntityService(entity_repository) - observation_service = ObservationService(observation_repository) - relation_service = RelationService(relation_repository) knowledge_sync_service = KnowledgeSyncService( - entity_service, observation_service, relation_service + entity_repository, observation_repository, relation_repository ) - knowledge_parser = KnowledgeParser() + entity_parser = EntityParser(config.home) search_service = SearchService(search_repository, entity_service) @@ -75,7 +71,7 @@ async def get_sync_service(db_type=DatabaseType.FILESYSTEM): sync_service = SyncService( scanner=file_change_scanner, knowledge_sync_service=knowledge_sync_service, - knowledge_parser=knowledge_parser, + entity_parser=entity_parser, search_service=search_service, ) @@ -147,9 +143,9 @@ def display_sync_summary(knowledge: SyncReport): # Format as: "Synced X files (A new, B modified, C deleted)" changes = [] - new_count = len(knowledge.new) - mod_count = len(knowledge.modified) - del_count = len(knowledge.deleted) + new_count = len(knowledge.new) + mod_count = len(knowledge.modified) + del_count = len(knowledge.deleted) if new_count: changes.append(f"[green]{new_count} new[/green]") diff --git a/src/basic_memory/sync/knowledge_sync_service.py b/src/basic_memory/sync/knowledge_sync_service.py index fcec617e..357cab99 100644 --- a/src/basic_memory/sync/knowledge_sync_service.py +++ b/src/basic_memory/sync/knowledge_sync_service.py @@ -1,13 +1,10 @@ """Service for managing entities in the database.""" -from typing import Dict - from loguru import logger from basic_memory.models import Entity as EntityModel, Observation, Relation, ObservationCategory from basic_memory.markdown.schemas import EntityMarkdown -from basic_memory.schemas.request import ObservationCreate -from basic_memory.services import EntityService, ObservationService, RelationService +from basic_memory.repository import EntityRepository, ObservationRepository, RelationRepository def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> EntityModel: @@ -23,7 +20,7 @@ def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> Enti if not obs.category or obs.category not in [c.value for c in ObservationCategory]: return ObservationCategory.NOTE.value return obs.category - + model = EntityModel( name=markdown.frontmatter.title, entity_type=markdown.frontmatter.type, @@ -32,11 +29,7 @@ def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> Enti content_type="text/markdown", summary=markdown.content.content, observations=[ - Observation( - content=obs.content, - category=get_valid_category(obs), - context=obs.context - ) + Observation(content=obs.content, category=get_valid_category(obs), context=obs.context) for obs in markdown.content.observations ], ) @@ -48,18 +41,20 @@ class KnowledgeSyncService: def __init__( self, - entity_service: EntityService, - observation_service: ObservationService, - relation_service: RelationService, + entity_repository: EntityRepository, + observation_repository: ObservationRepository, + relation_repository: RelationRepository, ): - self.entity_service = entity_service - self.observation_service = observation_service - self.relation_service = relation_service + self.entity_repository = entity_repository + self.observation_repository = observation_repository + self.relation_repository = relation_repository async def delete_entity_by_file_path(self, file_path: str) -> bool: - return await self.entity_service.delete_entity_by_file_path(file_path) + return await self.entity_repository.delete_by_file_path(file_path) - async def create_entity_and_observations(self, file_path: str, markdown: EntityMarkdown) -> EntityModel: + async def create_entity_and_observations( + self, file_path: str, markdown: EntityMarkdown + ) -> EntityModel: """First pass: Create entity and observations only. Creates the entity with null checksum to indicate sync not complete. @@ -68,7 +63,7 @@ class KnowledgeSyncService: logger.debug(f"Creating entity without relations: {markdown.frontmatter.id}") model = entity_model_from_markdown(file_path, markdown) model.checksum = None # Mark as incomplete sync - return await self.entity_service.add(model) + return await self.entity_repository.add(model) async def update_entity_and_observations( self, path_id: str, markdown: EntityMarkdown @@ -78,28 +73,33 @@ class KnowledgeSyncService: Updates everything except relations and sets null checksum to indicate sync not complete. """ - logger.debug(f"Updating entity without relations: {path_id}") - db_entity = await self.entity_service.get_by_path_id(path_id) + logger.debug(f"Updating entity and observations: {path_id}") + db_entity = await self.entity_repository.get_by_path_id(path_id) # Update fields from markdown db_entity.name = markdown.frontmatter.title db_entity.entity_type = markdown.frontmatter.type db_entity.summary = markdown.content.content - # Clear and update observations - await self.observation_service.delete_by_entity(db_entity.id) + # Clear observations for entity + await self.observation_repository.delete_by_fields(entity_id=db_entity.id) + + # add new observations observations = [ - Observation(entity_id=db_entity.id, - content=obs.content, - category=obs.category, - context=obs.context) for obs in markdown.content.observations + Observation( + entity_id=db_entity.id, + content=obs.content, + category=obs.category, + context=obs.context, + ) + for obs in markdown.content.observations ] - await self.observation_service.add_all(observations) + await self.observation_repository.add_all(observations) # update entity # checksum value is None == not finished with sync - return await self.entity_service.update_entity( - db_entity.path_id, + return await self.entity_repository.update( + db_entity.id, { "name": db_entity.name, "entity_type": db_entity.entity_type, @@ -117,18 +117,18 @@ class KnowledgeSyncService: checksum: Final checksum to set after relations are updated """ logger.debug(f"Updating relations for entity: {markdown.frontmatter.id}") - db_entity = await self.entity_service.get_by_path_id(markdown.frontmatter.id) + db_entity = await self.entity_repository.get_by_path_id(markdown.frontmatter.id) # get all entities from relations target_entity_path_ids = [rel.target for rel in markdown.content.relations] - target_entities = await self.entity_service.open_nodes(target_entity_path_ids) + target_entities = await self.entity_repository.find_by_path_ids(target_entity_path_ids) # dict by path entity_by_path = {e.path_id: e for e in target_entities} # Clear and update relations - await self.relation_service.delete_outgoing_relations_from_entity(db_entity.id) - + await self.relation_repository.delete_outgoing_relations_from_entity(db_entity.id) + # Use dict to deduplicate relations keyed by (target, type) relation_dict = {} for rel in markdown.content.relations: @@ -137,7 +137,7 @@ class KnowledgeSyncService: to_id = entity_by_path[rel.target].id key = (to_id, rel.type) - + # Only keep the first instance of each relation type to a target if key not in relation_dict: relation_dict[key] = Relation( @@ -152,9 +152,7 @@ class KnowledgeSyncService: ) # Create unique relations - await self.relation_service.create_relations(list(relation_dict.values())) + await self.relation_repository.add_all(relation_dict.values()) # Set final checksum to mark sync complete - return await self.entity_service.update_entity( - db_entity.path_id, {"checksum": checksum} - ) + return await self.entity_repository.update(db_entity.id, {"checksum": checksum}) diff --git a/tests/conftest.py b/tests/conftest.py index 5ceee94c..c964f064 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,12 +159,12 @@ async def activity_service(document_service, entity_service, relation_service): @pytest_asyncio.fixture async def knowledge_sync_service( - entity_service: EntityService, - observation_service: ObservationService, - relation_service: RelationService, + entity_repository: EntityRepository, + observation_repository: ObservationRepository, + relation_repository: RelationRepository, ) -> KnowledgeSyncService: """Create EntitySyncService with repository.""" - return KnowledgeSyncService(entity_service, observation_service, relation_service) + return KnowledgeSyncService(entity_repository, observation_repository, relation_repository) @pytest_asyncio.fixture diff --git a/tests/mcp/test_tool_create_relations.py b/tests/mcp/test_tool_create_relations.py index f02087c0..716e9575 100644 --- a/tests/mcp/test_tool_create_relations.py +++ b/tests/mcp/test_tool_create_relations.py @@ -22,7 +22,7 @@ async def test_create_basic_relation(client): # Create relation between them relation_request = CreateRelationsRequest( relations=[ - Relation(from_path_id="source_entity", to_path_id="target_entity", relation_type="depends_on") + Relation(from_id="source_entity", to_id="target_entity", relation_type="depends_on") ] ) result = await create_relations(relation_request) @@ -65,8 +65,8 @@ async def test_create_relation_with_context(client): relation_request = CreateRelationsRequest( relations=[ Relation( - from_path_id="source", - to_path_id="target", + from_id="source", + to_id="target", relation_type="implements", context="Implementation details", ) @@ -99,8 +99,8 @@ async def test_create_multiple_relations(client): relation_request = CreateRelationsRequest( relations=[ - Relation(from_path_id="entity1", to_path_id="entity2", relation_type="connects_to"), - Relation(from_path_id="entity2", to_path_id="entity3", relation_type="depends_on"), + Relation(from_id="entity1", to_id="entity2", relation_type="connects_to"), + Relation(from_id="entity2", to_id="entity3", relation_type="depends_on"), ] ) result = await create_relations(relation_request) @@ -140,8 +140,8 @@ async def test_create_bidirectional_relations(client): # Create relations in both directions relation_request = CreateRelationsRequest( relations=[ - Relation(from_path_id="service", to_path_id="database", relation_type="depends_on"), - Relation(from_path_id="database", to_path_id="service", relation_type="supports"), + Relation(from_id="service", to_id="database", relation_type="depends_on"), + Relation(from_id="database", to_id="service", relation_type="supports"), ] ) result = await create_relations(relation_request) @@ -171,7 +171,7 @@ async def test_create_relation_with_invalid_entity(client): relation_request = CreateRelationsRequest( relations=[ - Relation(from_path_id="real_entity", to_path_id="non_existent_entity", relation_type="depends_on") + Relation(from_id="real_entity", to_id="non_existent_entity", relation_type="depends_on") ] ) @@ -193,7 +193,7 @@ async def test_create_duplicate_relation(client): await create_entities(entity_request) # Create relation - relation = Relation(from_path_id="source", to_path_id="target", relation_type="connects_to") + relation = Relation(from_id="source", to_id="target", relation_type="connects_to") relation_request = CreateRelationsRequest(relations=[relation]) # Create first relation diff --git a/tests/mcp/test_tool_get_entity.py b/tests/mcp/test_tool_get_entity.py index 3c1db18e..9a44e126 100644 --- a/tests/mcp/test_tool_get_entity.py +++ b/tests/mcp/test_tool_get_entity.py @@ -90,7 +90,7 @@ async def test_get_entity_with_relations(client): relation_request = CreateRelationsRequest( relations=[ - Relation(from_path_id="source_entity", to_path_id="target_entity", relation_type="depends_on") + Relation(from_id="source_entity", to_id="target_entity", relation_type="depends_on") ] ) await create_relations(relation_request) diff --git a/tests/mcp/test_tool_open_nodes.py b/tests/mcp/test_tool_open_nodes.py index 4ef71688..c3880d9b 100644 --- a/tests/mcp/test_tool_open_nodes.py +++ b/tests/mcp/test_tool_open_nodes.py @@ -85,7 +85,7 @@ async def test_open_nodes_with_relations(client): from basic_memory.schemas.base import Relation relation_request = CreateRelationsRequest( - relations=[Relation(from_path_id=path_ids[0], to_path_id=path_ids[1], relation_type="depends_on")] + relations=[Relation(from_id=path_ids[0], to_id=path_ids[1], relation_type="depends_on")] ) await create_relations(relation_request) diff --git a/tests/services/test_relation_service.py b/tests/services/test_relation_service.py index 55d7368b..1e119e19 100644 --- a/tests/services/test_relation_service.py +++ b/tests/services/test_relation_service.py @@ -49,10 +49,10 @@ async def test_create_relations( relation_data = [ RelationSchema( - from_path_id=entity1.path_id, to_path_id=entity2.path_id, relation_type="type_0", context="context_0" + from_id=entity1.path_id, to_id=entity2.path_id, relation_type="type_0", context="context_0" ), RelationSchema( - from_path_id=entity1.path_id, to_path_id=entity2.path_id, relation_type="type_1", context="context_1" + from_id=entity1.path_id, to_id=entity2.path_id, relation_type="type_1", context="context_1" ), ] @@ -109,7 +109,7 @@ async def test_delete_relation( # Create a relation first relation_data = RelationSchema( - from_path_id=entity1.path_id, to_path_id=entity2.path_id, relation_type="test_relation" + from_id=entity1.path_id, to_id=entity2.path_id, relation_type="test_relation" ) await relation_service.create_relations([relation_data]) @@ -140,10 +140,10 @@ async def test_delete_relations_by_criteria( # Create test relations relation1 = RelationSchema( - from_path_id=entity1.path_id, to_path_id=entity2.path_id, relation_type="relation1" + from_id=entity1.path_id, to_id=entity2.path_id, relation_type="relation1" ) relation2 = RelationSchema( - from_path_id=entity1.path_id, to_path_id=entity2.path_id, relation_type="relation2" + from_id=entity1.path_id, to_id=entity2.path_id, relation_type="relation2" ) await relation_service.create_relations([relation1, relation2]) diff --git a/tests/sync/test_knowledge_sync_service.py b/tests/sync/test_knowledge_sync_service.py index 8159c4a8..2067b21e 100644 --- a/tests/sync/test_knowledge_sync_service.py +++ b/tests/sync/test_knowledge_sync_service.py @@ -130,8 +130,8 @@ async def test_update_entity_relations( file_path="concept/another_entity.md", content_type="text/markdown", ) - await knowledge_sync_service.entity_service.add(other_entity) - await knowledge_sync_service.entity_service.add(another_entity) + await knowledge_sync_service.entity_repository.add(other_entity) + await knowledge_sync_service.entity_repository.add(another_entity) # Update relations and set checksum test_checksum = "test-checksum-123" @@ -175,8 +175,8 @@ async def test_two_pass_sync_flow( file_path="concept/another_entity.md", content_type="text/markdown", ) - await knowledge_sync_service.entity_service.add(other_entity) - await knowledge_sync_service.entity_service.add(another_entity) + await knowledge_sync_service.entity_repository.add(other_entity) + await knowledge_sync_service.entity_repository.add(another_entity) # First pass - create without relations entity = await knowledge_sync_service.create_entity_and_observations("test.md", test_markdown) diff --git a/tests/sync/test_sync_knowledge.py b/tests/sync/test_sync_knowledge.py index 0461e54d..889fa0e7 100644 --- a/tests/sync/test_sync_knowledge.py +++ b/tests/sync/test_sync_knowledge.py @@ -103,7 +103,7 @@ modified: 2024-01-01 await sync_service.sync(test_config) # Verify entity created but no relations - entity = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/depends_on_future" ) assert entity is not None @@ -157,10 +157,10 @@ modified: 2024-01-01 await sync_service.sync(test_config) # Verify both entities and their relations - entity_a = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity_a = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/entity_a" ) - entity_b = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity_b = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/entity_b" ) @@ -232,7 +232,7 @@ modified: 2024-01-01 await sync_service.sync(test_config) # Verify duplicates are handled - entity = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/duplicate_relations" ) @@ -274,7 +274,7 @@ modified: 2024-01-01 await sync_service.sync(test_config) # Verify observations - entity = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/invalid_category" ) @@ -353,13 +353,13 @@ modified: 2024-01-01 await sync_service.sync(test_config) # Verify all relations are created correctly regardless of order - entity_a = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity_a = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/entity_a" ) - entity_b = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity_b = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/entity_b" ) - entity_c = await sync_service.knowledge_sync_service.entity_service.get_by_path_id( + entity_c = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( "concept/entity_c" ) diff --git a/tests/sync/test_sync_service.py b/tests/sync/test_sync_service.py index 855e1254..602349b8 100644 --- a/tests/sync/test_sync_service.py +++ b/tests/sync/test_sync_service.py @@ -58,7 +58,7 @@ modified: 2024-01-01 await asyncio.gather(sync_service.sync(test_config), modify_file()) # Verify final state - doc = await sync_service.knowledge_sync_service.entity_service.get_by_path_id("changing") + doc = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id("changing") assert doc is not None # File should have a checksum, even if it's from either version assert doc.checksum is not None