From c5d21276def72ed4688d141091c690351907cd63 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 12 Jan 2025 16:47:21 -0600 Subject: [PATCH] change entity.path_id to entity.permalink --- .../api/routers/knowledge_router.py | 46 ++++--- src/basic_memory/markdown/entity_parser.py | 14 +- src/basic_memory/markdown/knowledge_writer.py | 2 +- src/basic_memory/mcp/tools/activity.py | 32 ++--- src/basic_memory/mcp/tools/knowledge.py | 56 ++++---- src/basic_memory/mcp/tools/search.py | 23 ++-- src/basic_memory/models/knowledge.py | 17 +-- src/basic_memory/models/search.py | 4 +- .../repository/entity_repository.py | 42 +++--- .../repository/relation_repository.py | 15 ++- .../repository/search_repository.py | 52 ++++---- src/basic_memory/schemas/activity.py | 30 +++-- src/basic_memory/schemas/base.py | 60 ++++----- src/basic_memory/schemas/delete.py | 4 +- src/basic_memory/schemas/request.py | 23 ++-- src/basic_memory/schemas/response.py | 35 +++-- src/basic_memory/schemas/search.py | 10 +- src/basic_memory/services/activity_service.py | 77 ++++++----- src/basic_memory/services/entity_service.py | 44 +++---- src/basic_memory/services/file_service.py | 13 +- .../services/observation_service.py | 26 ++-- src/basic_memory/services/relation_service.py | 26 ++-- src/basic_memory/services/search_service.py | 14 +- src/basic_memory/sync/entity_sync_service.py | 22 ++-- src/basic_memory/sync/file_change_scanner.py | 42 +++--- src/basic_memory/sync/sync_service.py | 6 +- tests/api/test_discovery_router.py | 16 +-- tests/api/test_knowledge_router.py | 123 +++++++++--------- tests/api/test_search_router.py | 11 +- tests/cli/test_status.py | 10 +- tests/conftest.py | 2 +- tests/markdown/test_knowledge_writer.py | 4 +- tests/mcp/test_tool_add_observations.py | 22 ++-- tests/mcp/test_tool_create_entities.py | 4 +- tests/mcp/test_tool_create_relations.py | 18 +-- tests/mcp/test_tool_discovery.py | 2 +- tests/mcp/test_tool_get_entity.py | 17 +-- tests/mcp/test_tool_open_nodes.py | 34 ++--- tests/repository/test_entity_repository.py | 62 +++++---- .../repository/test_observation_repository.py | 12 +- tests/repository/test_relation_repository.py | 10 +- tests/schemas/test_activity_schemas.py | 40 +++--- tests/schemas/test_observation_schemas.py | 2 +- tests/schemas/test_schemas.py | 57 ++++---- tests/services/test_entity_service.py | 93 ++++++------- tests/services/test_observation_service.py | 33 ++--- tests/services/test_relation_service.py | 26 ++-- tests/services/test_search_service.py | 8 +- tests/sync/test_file_change_scanner.py | 16 ++- tests/sync/test_knowledge_sync_service.py | 16 +-- tests/sync/test_sync_knowledge.py | 22 ++-- tests/sync/test_sync_service.py | 6 +- 52 files changed, 714 insertions(+), 687 deletions(-) diff --git a/src/basic_memory/api/routers/knowledge_router.py b/src/basic_memory/api/routers/knowledge_router.py index fb29c4fa..a48265e1 100644 --- a/src/basic_memory/api/routers/knowledge_router.py +++ b/src/basic_memory/api/routers/knowledge_router.py @@ -5,7 +5,10 @@ from loguru import logger from basic_memory.deps import ( EntityServiceDep, - get_search_service, RelationServiceDep, ObservationServiceDep, FileServiceDep, + get_search_service, + RelationServiceDep, + ObservationServiceDep, + FileServiceDep, ) from basic_memory.schemas import ( CreateEntityRequest, @@ -47,9 +50,9 @@ async def create_entities( ) -@router.put("/entities/{path_id:path}", response_model=EntityResponse) +@router.put("/entities/{permalink:path}", response_model=EntityResponse) async def update_entity( - path_id: PathId, + permalink: PathId, data: UpdateEntityRequest, background_tasks: BackgroundTasks, entity_service: EntityServiceDep, @@ -61,7 +64,7 @@ async def update_entity( update_data = data.model_dump(exclude_none=True) # Update the entity - updated_entity = await entity_service.update_entity(path_id, **update_data) + updated_entity = await entity_service.update_entity(permalink, **update_data) # Reindex since content changed await search_service.index_entity(updated_entity, background_tasks=background_tasks) @@ -69,7 +72,7 @@ async def update_entity( return EntityResponse.model_validate(updated_entity) except EntityNotFoundError: - raise HTTPException(status_code=404, detail=f"Entity with {path_id} not found") + raise HTTPException(status_code=404, detail=f"Entity with {permalink} not found") @router.post("/relations", response_model=EntityListResponse) @@ -99,9 +102,9 @@ async def add_observations( search_service=Depends(get_search_service), ) -> EntityResponse: """Add observations to an entity and update search index.""" - logger.debug(f"Adding observations to entity: {data.path_id}") + logger.debug(f"Adding observations to entity: {data.permalink}") updated_entity = await observation_service.add_observations( - data.path_id, data.observations, data.context + data.permalink, data.observations, data.context ) # Reindex the entity with new observations @@ -113,22 +116,22 @@ async def add_observations( ## Read endpoints -@router.get("/entities/{path_id:path}", response_model=EntityResponse) +@router.get("/entities/{permalink:path}", response_model=EntityResponse) async def get_entity( - entity_service: EntityServiceDep, - file_service: FileServiceDep, - path_id: PathId, - content: bool = False, # New parameter + entity_service: EntityServiceDep, + file_service: FileServiceDep, + permalink: PathId, + content: bool = False, # New parameter ) -> EntityResponse: """Get a specific entity by ID. Args: - path_id: Entity path ID + permalink: Entity path ID content: If True, include full file content :param entity_service: EntityService """ try: - entity = await entity_service.get_by_path_id(path_id) + entity = await entity_service.get_by_permalink(permalink) entity_response = EntityResponse.model_validate(entity) if content: # Load content if requested @@ -137,14 +140,15 @@ async def get_entity( return entity_response except EntityNotFoundError: - raise HTTPException(status_code=404, detail=f"Entity with {path_id} not found") + raise HTTPException(status_code=404, detail=f"Entity with {permalink} not found") + @router.post("/nodes", response_model=EntityListResponse) async def open_nodes( data: OpenNodesRequest, entity_service: EntityServiceDep ) -> EntityListResponse: """Open specific nodes""" - entities = await entity_service.open_nodes(data.path_ids) + entities = await entity_service.open_nodes(data.permalinks) return EntityListResponse( entities=[EntityResponse.model_validate(entity) for entity in entities] ) @@ -161,11 +165,11 @@ async def delete_entities( search_service=Depends(get_search_service), ) -> DeleteEntitiesResponse: """Delete entities and remove from search index.""" - deleted = await entity_service.delete_entities(data.path_ids) + deleted = await entity_service.delete_entities(data.permalinks) # Remove each deleted entity from search index - for path_id in data.path_ids: - background_tasks.add_task(search_service.delete_by_path_id, path_id) + for permalink in data.permalinks: + background_tasks.add_task(search_service.delete_by_permalink, permalink) return DeleteEntitiesResponse(deleted=deleted) @@ -178,8 +182,8 @@ async def delete_observations( search_service=Depends(get_search_service), ) -> EntityResponse: """Delete observations and update search index.""" - path_id = data.path_id - updated_entity = await observation_service.delete_observations(path_id, data.observations) + permalink = data.permalink + updated_entity = await observation_service.delete_observations(permalink, data.observations) # Reindex the entity since observations changed await search_service.index_entity(updated_entity, background_tasks=background_tasks) diff --git a/src/basic_memory/markdown/entity_parser.py b/src/basic_memory/markdown/entity_parser.py index e03f717e..75af08e0 100644 --- a/src/basic_memory/markdown/entity_parser.py +++ b/src/basic_memory/markdown/entity_parser.py @@ -24,14 +24,12 @@ class EntityParser: """Parser for markdown files into Entity objects.""" def __init__(self, base_path: Path): - """Initialize parser with base path for relative path_id generation.""" + """Initialize parser with base path for relative permalink generation.""" self.base_path = base_path.resolve() - self.md = (MarkdownIt() - .use(observation_plugin) - .use(relation_plugin)) + self.md = MarkdownIt().use(observation_plugin).use(relation_plugin) - def get_path_id(self, file_path: Path) -> str: - """Get path_id from file path relative to base_path. + def get_permalink(self, file_path: Path) -> str: + """Get permalink from file path relative to base_path. Example: base_path: /project/root @@ -61,13 +59,13 @@ class EntityParser: post = frontmatter.load(str(file_path)) # Extract or generate required fields - path_id = post.metadata.get("id") or self.get_path_id(file_path) + permalink = post.metadata.get("id") or self.get_permalink(file_path) stats = file_path.stat() # Parse frontmatter entity_frontmatter = EntityFrontmatter( type=str(post.metadata.get("type", "note")), - id=path_id, + id=permalink, title=str(post.metadata.get("title", file_path.name)), created=self.parse_date(post.metadata.get("created")) or datetime.fromtimestamp(stats.st_ctime), diff --git a/src/basic_memory/markdown/knowledge_writer.py b/src/basic_memory/markdown/knowledge_writer.py index d9af8273..a5bbc554 100644 --- a/src/basic_memory/markdown/knowledge_writer.py +++ b/src/basic_memory/markdown/knowledge_writer.py @@ -16,7 +16,7 @@ class KnowledgeWriter: async def format_frontmatter(self, entity: EntityModel) -> dict: """Generate frontmatter metadata for entity.""" frontmatter = { - "id": entity.path_id, + "id": entity.permalink, "type": entity.entity_type, "created": entity.created_at.isoformat(), "modified": entity.updated_at.isoformat(), diff --git a/src/basic_memory/mcp/tools/activity.py b/src/basic_memory/mcp/tools/activity.py index ce64d39f..0a30732d 100644 --- a/src/basic_memory/mcp/tools/activity.py +++ b/src/basic_memory/mcp/tools/activity.py @@ -30,7 +30,7 @@ print(f"Relations changed: {activity.summary.relation_changes}") print("\\nMost active paths:") for path in activity.summary.most_active_paths: print(f"- {path}") -""" +""", }, { "name": "Document Changes", @@ -44,9 +44,9 @@ docs = await get_recent_activity( # Show document evolution chronologically for change in sorted(docs.changes, key=lambda x: x.timestamp): - print(f"{change.timestamp}: {change.path_id}") + print(f"{change.timestamp}: {change.permalink}") print(f" {change.change_type}: {change.summary}") -""" +""", }, { "name": "Knowledge Evolution", @@ -69,16 +69,16 @@ for type_, changes in changes_by_type.items(): entity_changes = defaultdict(int) for change in weekly.changes: if change.activity_type == "entity": - entity_changes[change.path_id] += 1 + entity_changes[change.permalink] += 1 print("\\nMost active entities:") -for path_id, count in sorted( +for permalink, count in sorted( entity_changes.items(), key=lambda x: x[1], reverse=True )[:5]: - print(f"- {path_id}: {count} changes") -""" + print(f"- {permalink}: {count} changes") +""", }, { "name": "Context Building", @@ -89,14 +89,14 @@ activity = await get_recent_activity(timeframe="1d") # Extract changed entities for deeper analysis entity_ids = [ - change.path_id for change in activity.changes + change.permalink for change in activity.changes if change.activity_type == "entity" ] # Load full entity details if entity_ids: entities = await open_nodes( - request=OpenNodesRequest(path_ids=entity_ids) + request=OpenNodesRequest(permalinks=entity_ids) ) # Analyze recent development focus @@ -112,27 +112,27 @@ if entity_ids: print(f"\\n{name}:") for obs in observations: print(f"- {obs.content}") -""" - } +""", + }, ], - output_model=RecentActivity + output_model=RecentActivity, ) async def get_recent_activity( timeframe: str = "1d", activity_types: Optional[List[ActivityType]] = None, ) -> RecentActivity: """Track changes across the knowledge base. - + Args: timeframe: Time window to analyze ("1h", "1d", "1w") activity_types: Optional list of types to filter by - + Returns: RecentActivity object with changes and summary statistics """ logger.debug(f"Getting recent activity (timeframe={timeframe}, types={activity_types})") - # Build params + # Build params params = { "timeframe": timeframe, } @@ -141,4 +141,4 @@ async def get_recent_activity( # Get activity response = await client.get("/activity/recent", params=params) - return RecentActivity.model_validate(response.json()) \ No newline at end of file + return RecentActivity.model_validate(response.json()) diff --git a/src/basic_memory/mcp/tools/knowledge.py b/src/basic_memory/mcp/tools/knowledge.py index a0d3ca87..ee50d518 100644 --- a/src/basic_memory/mcp/tools/knowledge.py +++ b/src/basic_memory/mcp/tools/knowledge.py @@ -42,7 +42,7 @@ await create_entities({ ] }] }) -""" +""", }, { "name": "Create Feature", @@ -61,10 +61,10 @@ await create_entities({ ] }] }) -""" - } +""", + }, ], - output_model=EntityListResponse + output_model=EntityListResponse, ) async def create_entities(request: CreateEntityRequest) -> EntityListResponse: """Create new entities in the knowledge graph.""" @@ -90,7 +90,7 @@ await create_relations({ "context": "Needs storage for search indexes" }] }) -""" +""", }, { "name": "Link Implementation", @@ -105,10 +105,10 @@ await create_relations({ "context": "Primary search implementation" }] }) -""" - } +""", + }, ], - output_model=EntityListResponse + output_model=EntityListResponse, ) async def create_relations(request: CreateRelationsRequest) -> EntityListResponse: """Create relations between existing entities.""" @@ -141,29 +141,29 @@ deps = [rel for rel in component.relations print("\\nDependencies:") for dep in deps: print(f"- {dep.to_id}") -""" +""", } ], - output_model=EntityResponse + output_model=EntityResponse, ) -async def get_entity(path_id: PathId, content: bool = False) -> EntityResponse: - """Get a specific entity by its path_id. - +async def get_entity(permalink: PathId, content: bool = False) -> EntityResponse: + """Get a specific entity by its permalink. + Args: - path_id: Path identifier for the entity + permalink: Path identifier for the entity content: If True, includes the full markdown content of the entity """ try: - url = f"/knowledge/entities/{path_id}" + url = f"/knowledge/entities/{permalink}" params = {"content": "true"} if content else {} response = await client.get(url, params=params) if response.status_code == 404: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") response.raise_for_status() return EntityResponse.model_validate(response.json()) except httpx.HTTPStatusError as e: if e.response.status_code == 404: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") raise @@ -177,7 +177,7 @@ async def get_entity(path_id: PathId, content: bool = False) -> EntityResponse: # Add technical observations await add_observations( request=AddObservationsRequest( - path_id="component/search_service", + permalink="component/search_service", context="Performance optimization", observations=[ ObservationCreate( @@ -195,10 +195,10 @@ await add_observations( ] ) ) -""" +""", } ], - output_model=EntityResponse + output_model=EntityResponse, ) async def add_observations(request: AddObservationsRequest) -> EntityResponse: """Add observations to an existing entity.""" @@ -217,17 +217,17 @@ async def add_observations(request: AddObservationsRequest) -> EntityResponse: # Remove old implementation notes await delete_observations( request=DeleteObservationsRequest( - path_id="component/indexer", + permalink="component/indexer", observations=[ "Using old indexing algorithm", "Temporary workaround for issue #123" ] ) ) -""" +""", } ], - output_model=EntityResponse + output_model=EntityResponse, ) async def delete_observations(request: DeleteObservationsRequest) -> EntityResponse: """Delete specific observations from an entity.""" @@ -253,10 +253,10 @@ await delete_relations( }] ) ) -""" +""", } ], - output_model=EntityListResponse + output_model=EntityListResponse, ) async def delete_relations(request: DeleteRelationsRequest) -> EntityListResponse: """Delete relations between entities.""" @@ -275,16 +275,16 @@ async def delete_relations(request: DeleteRelationsRequest) -> EntityListRespons # Remove deprecated components await delete_entities( request=DeleteEntitiesRequest( - path_ids=[ + permalinks=[ "component/old_service", "test/obsolete_test" ] ) ) -""" +""", } ], - output_model=Dict[str, bool] + output_model=Dict[str, bool], ) async def delete_entities(request: DeleteEntitiesRequest) -> DeleteEntitiesResponse: """Delete entities from the knowledge graph.""" diff --git a/src/basic_memory/mcp/tools/search.py b/src/basic_memory/mcp/tools/search.py index 4c8cac18..7215803a 100644 --- a/src/basic_memory/mcp/tools/search.py +++ b/src/basic_memory/mcp/tools/search.py @@ -28,7 +28,7 @@ by_type = defaultdict(list) for result in results.results: meta = result.metadata - path = result.path_id + path = result.permalink # Group by status if available if "status" in meta: @@ -78,7 +78,7 @@ sorted_results = sorted( print("Recent Changes:") for result in sorted_results: - print(f"\\n{result.path_id}") + print(f"\\n{result.permalink}") print(f"Type: {result.type}") print(f"Score: {result.score:.2f}") if "updated_at" in result.metadata: @@ -112,7 +112,7 @@ docs.sort(key=lambda x: x.score) print("Technical Documentation:") for doc in docs: - print(f"\\n{doc.path_id}") + print(f"\\n{doc.permalink}") if "title" in doc.metadata: print(f"Title: {doc.metadata['title']}") print(f"Score: {doc.score:.2f}") @@ -125,7 +125,7 @@ for doc in docs: "description": "Find content related to a specific entity", "code": """ # First get the entity to extract key terms -entity = await get_entity(path_id="component/memory_service") +entity = await get_entity(permalink="component/memory_service") if entity: # Build search terms from entity info @@ -143,12 +143,12 @@ if entity: ) # Filter out the original entity and sort by relevance - related = [r for r in results.results if r.path_id != entity["path_id"]] + related = [r for r in results.results if r.permalink != entity["permalink"]] related.sort(key=lambda x: x.score) print(f"Content Related to {entity['name']}:") for result in related[:5]: # Top 5 most relevant - print(f"\\n{result.path_id}") + print(f"\\n{result.permalink}") print(f"Type: {result.type}") print(f"Score: {result.score:.2f}") """, @@ -156,7 +156,6 @@ if entity: ], ) async def search(query: SearchQuery) -> SearchResponse: - """Search across all content in basic-memory. Args: @@ -175,7 +174,7 @@ async def search(query: SearchQuery) -> SearchResponse: @mcp.tool( category="search", - description="Load multiple entities by their path_ids in a single request", + description="Load multiple entities by their permalinks in a single request", examples=[ { "name": "Load and Analyze Entity Context", @@ -192,9 +191,9 @@ results = await search( if results.results: # Load full context for found entities - path_ids = [r.path_id for r in results.results] + permalinks = [r.permalink for r in results.results] context = await open_nodes( - request=OpenNodesRequest(path_ids=path_ids) + request=OpenNodesRequest(permalinks=permalinks) ) # Analyze relationships @@ -219,10 +218,10 @@ if results.results: ], ) async def open_nodes(request: OpenNodesRequest) -> EntityListResponse: - """Load multiple entities by their path_ids. + """Load multiple entities by their permalinks. Args: - request: OpenNodesRequest containing list of path_ids to load + request: OpenNodesRequest containing list of permalinks to load Returns: EntityListResponse containing complete details for each requested entity diff --git a/src/basic_memory/models/knowledge.py b/src/basic_memory/models/knowledge.py index 8855cfe1..b7c1adc0 100644 --- a/src/basic_memory/models/knowledge.py +++ b/src/basic_memory/models/knowledge.py @@ -13,7 +13,6 @@ from sqlalchemy import ( DateTime, Index, JSON, - CheckConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -27,14 +26,14 @@ class Entity(Base): Entities represent semantic nodes maintained by the AI layer. Each entity: - Has a unique numeric ID (database-generated) - - Maps to a file on disk + - Maps to a file on disk - Maintains a checksum for change detection - Tracks both source file and semantic properties """ __tablename__ = "entity" __table_args__ = ( - UniqueConstraint("path_id", name="uix_entity_path_id"), # Make path_id unique + UniqueConstraint("permalink", name="uix_entity_permalink"), # Make permalink unique Index("ix_entity_type", "entity_type"), Index("ix_entity_created_at", "created_at"), # For timeline queries Index("ix_entity_updated_at", "updated_at"), # For timeline queries @@ -48,12 +47,12 @@ class Entity(Base): content_type: Mapped[str] = mapped_column(String) # Normalized path for URIs - path_id: Mapped[str] = mapped_column(String, unique=True, index=True) + permalink: Mapped[str] = mapped_column(String, unique=True, index=True) # Actual filesystem relative path file_path: Mapped[str] = mapped_column(String, unique=True, index=True) # checksum of file checksum: Mapped[Optional[str]] = mapped_column(String, nullable=True) - + # Content summary summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True) @@ -96,6 +95,7 @@ class ObservationCategory(str, Enum): ISSUE = "issue" TODO = "todo" + class Observation(Base): """ An observation about an entity. @@ -122,12 +122,9 @@ class Observation(Base): ) context: Mapped[str] = mapped_column(Text, nullable=True) tags: Mapped[Optional[list[str]]] = mapped_column( - JSON, - nullable=True, - default=list, - server_default='[]' + JSON, nullable=True, default=list, server_default="[]" ) - + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=text("CURRENT_TIMESTAMP"), onupdate=text("CURRENT_TIMESTAMP") diff --git a/src/basic_memory/models/search.py b/src/basic_memory/models/search.py index 49b9132d..e0789845 100644 --- a/src/basic_memory/models/search.py +++ b/src/basic_memory/models/search.py @@ -6,10 +6,10 @@ from sqlalchemy import DDL CREATE_SEARCH_INDEX = DDL(""" CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5( content, -- Searchable text content - path_id UNINDEXED, -- Link to entity/document (must be unique) + permalink UNINDEXED, -- Link to entity/document (must be unique) file_path UNINDEXED, -- Filesystem path type UNINDEXED, -- 'entity' or 'document' metadata UNINDEXED, -- JSON with timestamps, types, etc. tokenize='porter unicode61' -- Enable stemming + unicode ); -""") \ No newline at end of file +""") diff --git a/src/basic_memory/repository/entity_repository.py b/src/basic_memory/repository/entity_repository.py index 1b9f4140..15e9d04e 100644 --- a/src/basic_memory/repository/entity_repository.py +++ b/src/basic_memory/repository/entity_repository.py @@ -2,7 +2,7 @@ from typing import List, Optional, Sequence -from sqlalchemy import select, or_, asc, desc +from sqlalchemy import select, or_, asc from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.orm import selectinload from sqlalchemy.orm.interfaces import LoaderOption @@ -18,16 +18,16 @@ class EntityRepository(Repository[Entity]): """Initialize with session maker.""" super().__init__(session_maker, Entity) - async def get_by_path_id(self, path_id: str) -> Optional[Entity]: - """Get entity by path_id.""" - query = self.select().where(Entity.path_id == path_id).options(*self.get_load_options()) + async def get_by_permalink(self, permalink: str) -> Optional[Entity]: + """Get entity by permalink.""" + query = self.select().where(Entity.permalink == permalink).options(*self.get_load_options()) return await self.find_one(query) async def list_entities( - self, - entity_type: Optional[str] = None, - sort_by: Optional[str] = "updated_at", - include_related: bool = False, + self, + entity_type: Optional[str] = None, + sort_by: Optional[str] = "updated_at", + include_related: bool = False, ) -> Sequence[Entity]: """List all entities, optionally filtered by type and sorted.""" query = self.select() @@ -44,8 +44,12 @@ class EntityRepository(Repository[Entity]): query = query.where( or_( Entity.entity_type == entity_type, - Entity.outgoing_relations.any(Relation.to_entity.has(entity_type=entity_type)), - Entity.incoming_relations.any(Relation.from_entity.has(entity_type=entity_type)) + Entity.outgoing_relations.any( + Relation.to_entity.has(entity_type=entity_type) + ), + Entity.incoming_relations.any( + Relation.from_entity.has(entity_type=entity_type) + ), ) ) else: @@ -111,28 +115,30 @@ class EntityRepository(Repository[Entity]): selectinload(Entity.incoming_relations).selectinload(Relation.to_entity), ] - async def find_by_path_ids(self, path_ids: List[str]) -> Sequence[Entity]: - """Find multiple entities by their path_id.""" + async def find_by_permalinks(self, permalinks: List[str]) -> Sequence[Entity]: + """Find multiple entities by their permalink.""" # Handle empty input explicitly - if not path_ids: + if not permalinks: return [] # Use existing select pattern - query = self.select().options(*self.get_load_options()).where(Entity.path_id.in_(path_ids)) + query = ( + self.select().options(*self.get_load_options()).where(Entity.permalink.in_(permalinks)) + ) result = await self.execute_query(query) return list(result.scalars().all()) - async def delete_by_path_ids(self, path_ids: List[str]) -> int: - """Delete multiple entities by path_id.""" + async def delete_by_permalinks(self, permalinks: List[str]) -> int: + """Delete multiple entities by permalink.""" # Handle empty input explicitly - if not path_ids: + if not permalinks: return 0 # Find matching entities - entities = await self.find_by_path_ids(path_ids) + entities = await self.find_by_permalinks(permalinks) if not entities: return 0 diff --git a/src/basic_memory/repository/relation_repository.py b/src/basic_memory/repository/relation_repository.py index 5b7c2d81..ea7ed5e7 100644 --- a/src/basic_memory/repository/relation_repository.py +++ b/src/basic_memory/repository/relation_repository.py @@ -1,4 +1,5 @@ """Repository for managing Relation objects.""" + from sqlalchemy import and_, delete from typing import Sequence, List, Optional @@ -18,7 +19,9 @@ class RelationRepository(Repository[Relation]): def __init__(self, session_maker: async_sessionmaker): super().__init__(session_maker, Relation) - async def find_relation(self, from_path_id: str, to_path_id: str, relation_type: str) -> Optional[Relation]: + async def find_relation( + self, from_permalink: str, to_permalink: str, relation_type: str + ) -> Optional[Relation]: """Find a relation by its from and to path IDs.""" from_entity = aliased(Entity) to_entity = aliased(Entity) @@ -29,9 +32,9 @@ class RelationRepository(Repository[Relation]): .join(to_entity, Relation.to_id == to_entity.id) .where( and_( - from_entity.path_id == from_path_id, - to_entity.path_id == to_path_id, - Relation.relation_type == relation_type + from_entity.permalink == from_permalink, + to_entity.permalink == to_permalink, + Relation.relation_type == relation_type, ) ) ) @@ -62,9 +65,7 @@ class RelationRepository(Repository[Relation]): as these are the ones owned by this entity's markdown file. """ async with db.scoped_session(self.session_maker) as session: - await session.execute( - delete(Relation).where(Relation.from_id == entity_id) - ) + await session.execute(delete(Relation).where(Relation.from_id == entity_id)) def get_load_options(self) -> List[LoaderOption]: return [selectinload(Relation.from_entity), selectinload(Relation.to_entity)] diff --git a/src/basic_memory/repository/search_repository.py b/src/basic_memory/repository/search_repository.py index 37b8e84b..4cbe96ab 100644 --- a/src/basic_memory/repository/search_repository.py +++ b/src/basic_memory/repository/search_repository.py @@ -2,19 +2,17 @@ import json from typing import List, Optional -from datetime import datetime from loguru import logger from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from basic_memory import db -from basic_memory.repository.repository import Repository from basic_memory.schemas.search import SearchQuery, SearchResult, SearchItemType from basic_memory.models.search import CREATE_SEARCH_INDEX -class SearchRepository(): +class SearchRepository: """Repository for search index operations.""" def __init__(self, session_maker: async_sessionmaker[AsyncSession]): @@ -27,9 +25,7 @@ class SearchRepository(): await session.commit() async def search( - self, - query: SearchQuery, - context: Optional[List[str]] = None + self, query: SearchQuery, context: Optional[List[str]] = None ) -> List[SearchResult]: """Search across all indexed content.""" conditions = [] @@ -48,23 +44,19 @@ class SearchRepository(): # Handle entity type filter if query.entity_types: entity_type_list = ", ".join(f"'{t}'" for t in query.entity_types) - conditions.append( - f"json_extract(metadata, '$.entity_type') IN ({entity_type_list})" - ) + conditions.append(f"json_extract(metadata, '$.entity_type') IN ({entity_type_list})") # Handle date filter if query.after_date: params["after_date"] = query.after_date - conditions.append( - "json_extract(metadata, '$.created_at') > :after_date" - ) + conditions.append("json_extract(metadata, '$.created_at') > :after_date") # Build WHERE clause where_clause = " AND ".join(conditions) if conditions else "1=1" - + sql = f""" SELECT - path_id, + permalink, file_path, type, metadata, @@ -80,11 +72,11 @@ class SearchRepository(): return [ SearchResult( - path_id=row.path_id, + permalink=row.permalink, file_path=row.file_path, type=SearchItemType(row.type), # Convert string to enum score=row.score, - metadata=json.loads(row.metadata) + metadata=json.loads(row.metadata), ) for row in rows ] @@ -92,44 +84,44 @@ class SearchRepository(): async def index_item( self, content: str, - path_id: str, + permalink: str, file_path: str, type: SearchItemType, # Now accepts enum - metadata: dict + metadata: dict, ): """Index or update a single item.""" async with db.scoped_session(self.session_maker) as session: # Delete existing record if any await session.execute( - text("DELETE FROM search_index WHERE path_id = :path_id"), - {"path_id": path_id} + text("DELETE FROM search_index WHERE permalink = :permalink"), + {"permalink": permalink}, ) # Insert new record await session.execute( text(""" INSERT INTO search_index ( - content, path_id, file_path, type, metadata + content, permalink, file_path, type, metadata ) VALUES ( - :content, :path_id, :file_path, :type, :metadata + :content, :permalink, :file_path, :type, :metadata ) """), { "content": content, - "path_id": path_id, + "permalink": permalink, "file_path": file_path, "type": type.value, # Store the string value - "metadata": json.dumps(metadata) - } + "metadata": json.dumps(metadata), + }, ) - logger.debug(f"indexed {path_id}") + logger.debug(f"indexed {permalink}") await session.commit() - async def delete_by_path_id(self, path_id: str): + async def delete_by_permalink(self, permalink: str): """Delete an item from the search index.""" async with db.scoped_session(self.session_maker) as session: await session.execute( - text("DELETE FROM search_index WHERE path_id = :path_id"), - {"path_id": path_id} + text("DELETE FROM search_index WHERE permalink = :permalink"), + {"permalink": permalink}, ) - await session.commit() \ No newline at end of file + await session.commit() diff --git a/src/basic_memory/schemas/activity.py b/src/basic_memory/schemas/activity.py index 57228545..3f9df3d8 100644 --- a/src/basic_memory/schemas/activity.py +++ b/src/basic_memory/schemas/activity.py @@ -7,34 +7,34 @@ from pydantic import BaseModel, Field class TimeFrame: """Represents a time period for querying activity.""" - + def __init__(self, timeframe_str: str): """Parse timeframe string (e.g., '1d', '2w', '1m')""" if not timeframe_str or len(timeframe_str) < 2: raise ValueError("Invalid timeframe format") - + try: self.value = int(timeframe_str[:-1]) self.unit = timeframe_str[-1] except ValueError: raise ValueError("Invalid timeframe format") - - if self.unit not in ['h', 'd', 'w', 'm']: + + if self.unit not in ["h", "d", "w", "m"]: raise ValueError("Invalid timeframe unit") - + if self.value < 1: raise ValueError("Timeframe value must be positive") - + @property def to_timedelta(self) -> timedelta: """Convert to Python timedelta.""" - if self.unit == 'h': + if self.unit == "h": return timedelta(hours=self.value) - elif self.unit == 'd': + elif self.unit == "d": return timedelta(days=self.value) - elif self.unit == 'w': + elif self.unit == "w": return timedelta(weeks=self.value) - elif self.unit == 'm': + elif self.unit == "m": # Approximate month as 30 days return timedelta(days=self.value * 30) else: @@ -43,12 +43,14 @@ class TimeFrame: class ActivityType(str, Enum): """Types of activities that can be tracked.""" + ENTITY = "entity" RELATION = "relation" class ChangeType(str, Enum): """Types of changes that can occur.""" + CREATED = "created" UPDATED = "updated" DELETED = "deleted" @@ -56,26 +58,28 @@ class ChangeType(str, Enum): class ActivityChange(BaseModel): """Represents a single change in the system.""" + activity_type: ActivityType change_type: ChangeType timestamp: datetime - path_id: str + permalink: str summary: str content: Optional[str] = None class ActivitySummary(BaseModel): """Summary statistics about recent activity.""" + entity_changes: int = Field(default=0, description="Number of entity changes") relation_changes: int = Field(default=0, description="Number of relation changes") most_active_paths: List[str] = Field( - default_factory=list, - description="List of most frequently changed paths" + default_factory=list, description="List of most frequently changed paths" ) class RecentActivity(BaseModel): """Complete activity report.""" + timeframe: str changes: List[ActivityChange] = Field(default_factory=list) summary: ActivitySummary diff --git a/src/basic_memory/schemas/base.py b/src/basic_memory/schemas/base.py index b294a372..57e25c15 100644 --- a/src/basic_memory/schemas/base.py +++ b/src/basic_memory/schemas/base.py @@ -10,6 +10,7 @@ Key Concepts: 3. Observations are atomic facts/notes about an entity 4. Everything is stored in both SQLite and markdown files """ + import mimetypes import re from enum import Enum @@ -29,7 +30,7 @@ def to_snake_case(name: str) -> str: Memory_Service -> memory_service """ name = name.strip() - + # Replace spaces and hyphens and . with underscores s1 = re.sub(r"[\s\-\\.]", "_", name) @@ -48,7 +49,6 @@ def validate_path_format(path: str) -> str: return path - class ObservationCategory(str, Enum): """Categories for structuring observations. @@ -62,6 +62,7 @@ class ObservationCategory(str, Enum): Categories are case-insensitive for easier use. """ + TECH = "tech" DESIGN = "design" FEATURE = "feature" @@ -76,16 +77,16 @@ class ObservationCategory(str, Enum): return cls(value.lower()) except ValueError: return None - - + + PathId = Annotated[str, BeforeValidator(to_snake_case), BeforeValidator(validate_path_format)] """Unique identifier in format '{path}/{normalized_name}'.""" Observation = Annotated[ - str, + str, BeforeValidator(str.strip), # Clean whitespace MinLen(1), # Ensure non-empty after stripping - MaxLen(1000) # Keep reasonable length + MaxLen(1000), # Keep reasonable length ] """A single piece of information about an entity. Must be non-empty and under 1000 characters. """ @@ -94,23 +95,22 @@ EntityType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(20 """Classification of entity (e.g., 'person', 'project', 'concept'). """ ALLOWED_CONTENT_TYPES = { - 'text/markdown', - 'text/plain', - 'application/pdf', - 'image/jpeg', - 'image/png', - 'image/svg+xml', + "text/markdown", + "text/plain", + "application/pdf", + "image/jpeg", + "image/png", + "image/svg+xml", } ContentType = Annotated[ - str, + str, BeforeValidator(str.lower), - Field(pattern=r'^[\w\-\+\.]+/[\w\-\+\.]+$'), - Field(json_schema_extra={"examples": list(ALLOWED_CONTENT_TYPES)}) + Field(pattern=r"^[\w\-\+\.]+/[\w\-\+\.]+$"), + Field(json_schema_extra={"examples": list(ALLOWED_CONTENT_TYPES)}), ] - RelationType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)] """Type of relationship between entities. Always use active voice present tense.""" @@ -119,7 +119,7 @@ class Relation(BaseModel): """Represents a directed edge between entities in the knowledge graph. Relations are directed connections stored in active voice (e.g., "created", "depends_on"). - The from_path_id represents the source or actor entity, while to_path_id represents the target + The from_permalink represents the source or actor entity, while to_permalink represents the target or recipient entity. """ @@ -133,7 +133,7 @@ class Entity(BaseModel): """Represents a node in our knowledge graph - could be a person, project, concept, etc. Each entity has: - - A title + - A title - An entity type (for classification) - A list of observations (facts/notes about the entity) - Optional relations to other entities @@ -147,32 +147,32 @@ class Entity(BaseModel): summary: Optional[str] = None content_type: ContentType = Field( description="MIME type of the content (e.g. text/markdown, image/jpeg)", - examples=["text/markdown", "image/jpeg"] - ) + examples=["text/markdown", "image/jpeg"], + ) observations: List[Observation] = [] @property - def path_id(self) -> PathId: + def permalink(self) -> PathId: """Get the path ID in format {snake_case_title}.""" normalized_name = to_snake_case(self.title) return normalized_name @property def file_path(self): - """Get the file path for this entity based on its path_id.""" - return f"{self.path_id}.md" + """Get the file path for this entity based on its permalink.""" + return f"{self.permalink}.md" - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def infer_content_type(cls, data: Dict) -> Dict: """Infer content_type from file_path if not provided.""" - if 'content_type' not in data: - # Get path from either file_path or construct from path_id - file_path = data.get('file_path') or f"{data.get('name')}.md" - + if "content_type" not in data: + # Get path from either file_path or construct from permalink + file_path = data.get("file_path") or f"{data.get('name')}.md" + if not file_path: raise ValidationError("Either file_path or name must be provided") mime_type, _ = mimetypes.guess_type(file_path) - data['content_type'] = mime_type or 'text/plain' + data["content_type"] = mime_type or "text/plain" - return data \ No newline at end of file + return data diff --git a/src/basic_memory/schemas/delete.py b/src/basic_memory/schemas/delete.py index 0d06c8d9..e0fdb2f5 100644 --- a/src/basic_memory/schemas/delete.py +++ b/src/basic_memory/schemas/delete.py @@ -34,7 +34,7 @@ class DeleteEntitiesRequest(BaseModel): 4. Deletes the corresponding markdown file """ - path_ids: Annotated[List[PathId], MinLen(1)] + permalinks: Annotated[List[PathId], MinLen(1)] class DeleteRelationsRequest(BaseModel): @@ -56,5 +56,5 @@ class DeleteObservationsRequest(BaseModel): match exactly for deletion. """ - path_id: PathId + permalink: PathId observations: Annotated[List[Observation], MinLen(1)] diff --git a/src/basic_memory/schemas/request.py b/src/basic_memory/schemas/request.py index 7a94a095..1f175097 100644 --- a/src/basic_memory/schemas/request.py +++ b/src/basic_memory/schemas/request.py @@ -3,16 +3,23 @@ from typing import List, Optional, Annotated, Dict, Any from annotated_types import MaxLen, MinLen -from pydantic import BaseModel, StringConstraints +from pydantic import BaseModel -from basic_memory.schemas.base import Observation, Entity, Relation, PathId, ObservationCategory, EntityType +from basic_memory.schemas.base import ( + Observation, + Entity, + Relation, + PathId, + ObservationCategory, + EntityType, +) class ObservationCreate(BaseModel): """A single observation with category, content, and optional context.""" category: ObservationCategory = ObservationCategory.NOTE - content: Observation + content: Observation class AddObservationsRequest(BaseModel): @@ -23,7 +30,7 @@ class AddObservationsRequest(BaseModel): to our understanding of the entity. """ - path_id: PathId + permalink: PathId context: Optional[str] = None observations: List[ObservationCreate] @@ -34,7 +41,7 @@ class CreateEntityRequest(BaseModel): Entities represent nodes in the knowledge graph. They can be created with initial observations and optional descriptions. Entity IDs are automatically generated from the type and name. - + Observations will be assigned the default category of 'note'. """ @@ -81,21 +88,21 @@ class OpenNodesRequest(BaseModel): discovered through search. """ - path_ids: Annotated[List[PathId], MinLen(1)] + permalinks: Annotated[List[PathId], MinLen(1)] class CreateRelationsRequest(BaseModel): - relations: List[Relation] ## update + class UpdateEntityRequest(BaseModel): """Request to update an existing entity.""" + title: Optional[str] = None entity_type: Optional[EntityType] = None summary: Optional[str] = None content: Optional[str] = None entity_metadata: Optional[Dict[str, Any]] = None - diff --git a/src/basic_memory/schemas/response.py b/src/basic_memory/schemas/response.py index fd27e6e5..74b03895 100644 --- a/src/basic_memory/schemas/response.py +++ b/src/basic_memory/schemas/response.py @@ -11,12 +11,11 @@ Key Features: 4. Bulk operations return all affected items """ -import datetime -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices -from basic_memory.schemas.base import Observation, Relation, PathId, Entity, EntityType, ContentType +from basic_memory.schemas.base import Relation, PathId, EntityType, ContentType from basic_memory.schemas.request import ObservationCreate @@ -44,6 +43,7 @@ class ObservationResponse(ObservationCreate, SQLAlchemyModel): "context": "Initial database design meeting" } """ + context: Optional[str] = None @@ -61,20 +61,21 @@ class RelationResponse(Relation, SQLAlchemyModel): "context": "Comprehensive test suite" } """ + from_id: PathId = Field( - # use the path_id from the associated Entity + # use the permalink from the associated Entity # or the from_id value validation_alias=AliasChoices( - AliasPath('from_entity', 'path_id'), - 'from_id', + AliasPath("from_entity", "permalink"), + "from_id", ) ) to_id: PathId = Field( - # use the path_id from the associated Entity + # use the permalink from the associated Entity # or the to_id value validation_alias=AliasChoices( - AliasPath('to_entity', 'path_id'), - 'to_id', + AliasPath("to_entity", "permalink"), + "to_id", ) ) @@ -90,7 +91,7 @@ class EntityResponse(SQLAlchemyModel): Example Response: { - "path_id": "component/memory_service", + "permalink": "component/memory_service", "title": "MemoryService", "entity_type": "component", "description": "Core persistence service", @@ -117,8 +118,8 @@ class EntityResponse(SQLAlchemyModel): } """ - # Note this Class does not inherit form Entity because of the Entity.path_id semantics - path_id: PathId + # Note this Class does not inherit form Entity because of the Entity.permalink semantics + permalink: PathId title: str entity_type: EntityType entity_metadata: Optional[Dict] = None @@ -133,14 +134,14 @@ class EntityListResponse(SQLAlchemyModel): """Response for create_entities operation. Returns complete information about entities returned from the service, - including their path_ids, observations, + including their permalinks, observations, and any established relations. Example Response: { "entities": [ { - "path_id": "component/search_service", + "permalink": "component/search_service", "title": "SearchService", "entity_type": "component", "description": "Knowledge graph search", @@ -152,7 +153,7 @@ class EntityListResponse(SQLAlchemyModel): "relations": [] }, { - "path_id": "document/api_docs", + "permalink": "document/api_docs", "title": "API_Documentation", "entity_type": "document", "description": "API Reference", @@ -180,7 +181,7 @@ class SearchNodesResponse(SQLAlchemyModel): { "matches": [ { - "path_id": "component/memory_service", + "permalink": "component/memory_service", "title": "MemoryService", "entity_type": "component", "description": "Core service", @@ -199,7 +200,6 @@ class SearchNodesResponse(SQLAlchemyModel): query: str - class DeleteEntitiesResponse(SQLAlchemyModel): """Response indicating successful entity deletion. @@ -213,4 +213,3 @@ class DeleteEntitiesResponse(SQLAlchemyModel): """ deleted: bool - diff --git a/src/basic_memory/schemas/search.py b/src/basic_memory/schemas/search.py index 461aacb5..f8b7d8cb 100644 --- a/src/basic_memory/schemas/search.py +++ b/src/basic_memory/schemas/search.py @@ -8,18 +8,20 @@ from pydantic import BaseModel, field_validator class SearchItemType(str, Enum): """Types of searchable items.""" + DOCUMENT = "document" ENTITY = "entity" class SearchQuery(BaseModel): """Search query parameters.""" + text: str types: Optional[List[SearchItemType]] = None entity_types: Optional[List[str]] = None after_date: Optional[Union[datetime, str]] = None - @field_validator('after_date') + @field_validator("after_date") @classmethod def validate_date(cls, v: Optional[Union[datetime, str]]) -> Optional[str]: """Convert datetime to ISO format if needed.""" @@ -32,7 +34,8 @@ class SearchQuery(BaseModel): class SearchResult(BaseModel): """Search result item.""" - path_id: str + + permalink: str file_path: str type: SearchItemType score: float @@ -41,4 +44,5 @@ class SearchResult(BaseModel): class SearchResponse(BaseModel): """Wrapper for search results list.""" - results: List[SearchResult] \ No newline at end of file + + results: List[SearchResult] diff --git a/src/basic_memory/services/activity_service.py b/src/basic_memory/services/activity_service.py index 40984157..09aa9f7a 100644 --- a/src/basic_memory/services/activity_service.py +++ b/src/basic_memory/services/activity_service.py @@ -1,7 +1,7 @@ """Service for tracking and querying activity across the knowledge base.""" from datetime import datetime, timezone -from typing import List, Optional, Sequence +from typing import List, Optional from . import EntityService, RelationService from ..schemas.activity import ( @@ -32,11 +32,11 @@ class ActivityService: activity_types: Optional[List[str]] = None, ) -> RecentActivity: """Get all recent activity in the knowledge base. - + Args: timeframe: Time window to look back (1h, 1d, 1w, 1m) activity_types: Optional list of types to include - + Returns: RecentActivity object containing changes and summary """ @@ -47,9 +47,7 @@ class ActivityService: # Get changes based on requested types changes = [] types_to_fetch = ( - [ActivityType(t) for t in activity_types] - if activity_types - else list(ActivityType) + [ActivityType(t) for t in activity_types] if activity_types else list(ActivityType) ) for activity_type in types_to_fetch: @@ -68,81 +66,92 @@ class ActivityService: summary = ActivitySummary( entity_changes=len([c for c in changes if c.activity_type == ActivityType.ENTITY]), relation_changes=len([c for c in changes if c.activity_type == ActivityType.RELATION]), - most_active_paths=self._get_most_active_paths(changes) + most_active_paths=self._get_most_active_paths(changes), ) - return RecentActivity( - timeframe=timeframe, - changes=changes, - summary=summary - ) + return RecentActivity(timeframe=timeframe, changes=changes, summary=summary) async def _get_entity_changes(self, since: datetime) -> List[ActivityChange]: """Get recent entity changes.""" # Query entities updated since the cutoff entities = await self.entity_service.get_modified_since(since) - + changes = [] for entity in entities: # Ensure timestamps are timezone-aware - created_at = entity.created_at.replace(tzinfo=timezone.utc) if entity.created_at.tzinfo is None else entity.created_at - updated_at = entity.updated_at.replace(tzinfo=timezone.utc) if entity.updated_at.tzinfo is None else entity.updated_at - + created_at = ( + entity.created_at.replace(tzinfo=timezone.utc) + if entity.created_at.tzinfo is None + else entity.created_at + ) + updated_at = ( + entity.updated_at.replace(tzinfo=timezone.utc) + if entity.updated_at.tzinfo is None + else entity.updated_at + ) + change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED - + changes.append( ActivityChange( activity_type=ActivityType.ENTITY, change_type=change_type, timestamp=updated_at, - path_id=entity.path_id, + permalink=entity.permalink, summary=f"{change_type.value.title()} entity: {entity.title}", - content=entity.summary + content=entity.summary, ) ) - - return changes + return changes async def _get_relation_changes(self, since: datetime) -> List[ActivityChange]: """Get recent relation changes.""" # Query relations updated since the cutoff relations = await self.relation_service.get_modified_since(since) - + changes = [] for relation in relations: # Ensure timestamps are timezone-aware - created_at = relation.created_at.replace(tzinfo=timezone.utc) if relation.created_at.tzinfo is None else relation.created_at - updated_at = relation.updated_at.replace(tzinfo=timezone.utc) if relation.updated_at.tzinfo is None else relation.updated_at - + created_at = ( + relation.created_at.replace(tzinfo=timezone.utc) + if relation.created_at.tzinfo is None + else relation.created_at + ) + updated_at = ( + relation.updated_at.replace(tzinfo=timezone.utc) + if relation.updated_at.tzinfo is None + else relation.updated_at + ) + change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED - + changes.append( ActivityChange( activity_type=ActivityType.RELATION, change_type=change_type, timestamp=updated_at, - path_id=f"{relation.from_id}->{relation.to_id}", + permalink=f"{relation.from_id}->{relation.to_id}", summary=( f"{change_type.value.title()} relation: " f"{relation.from_id} {relation.relation_type} {relation.to_id}" ), - content=relation.context + content=relation.context, ) ) - + return changes def _get_most_active_paths(self, changes: List[ActivityChange], limit: int = 5) -> List[str]: """Get the most frequently changed paths.""" path_counts = {} for change in changes: - path_counts[change.path_id] = path_counts.get(change.path_id, 0) + 1 - + path_counts[change.permalink] = path_counts.get(change.permalink, 0) + 1 + # Sort by count descending and take top paths sorted_paths = sorted( path_counts.items(), - key=lambda x: (-x[1], x[0]) # Sort by count desc, then path asc + key=lambda x: (-x[1], x[0]), # Sort by count desc, then path asc ) - - return [path for path, _ in sorted_paths[:limit]] \ No newline at end of file + + return [path for path, _ in sorted_paths[:limit]] diff --git a/src/basic_memory/services/entity_service.py b/src/basic_memory/services/entity_service.py index 60cbb699..8fe6bae5 100644 --- a/src/basic_memory/services/entity_service.py +++ b/src/basic_memory/services/entity_service.py @@ -17,7 +17,7 @@ def entity_model(entity: EntitySchema): title=entity.title, entity_type=entity.entity_type, entity_metadata=entity.entity_metadata, - path_id=entity.path_id, + permalink=entity.permalink, file_path=entity.file_path, summary=entity.summary, content_type=entity.content_type, @@ -57,7 +57,7 @@ class EntityService(BaseService[EntityModel]): except Exception as e: # Clean up on any failure if db_entity: - await self.delete_entity(db_entity.path_id) + await self.delete_entity(db_entity.permalink) await self.file_service.delete_entity_file(db_entity) logger.error(f"Failed to create entity: {e}") raise @@ -69,7 +69,7 @@ class EntityService(BaseService[EntityModel]): async def update_entity( self, - path_id: str, + permalink: str, content: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **update_fields: Any, @@ -77,7 +77,7 @@ class EntityService(BaseService[EntityModel]): """Update an entity's content and metadata. Args: - path_id: Entity's path ID + permalink: Entity's path ID content: Optional new content metadata: Optional metadata updates **update_fields: Additional entity fields to update @@ -88,12 +88,12 @@ class EntityService(BaseService[EntityModel]): Raises: EntityNotFoundError: If entity doesn't exist """ - logger.debug(f"Updating entity with path_id: {path_id}") + logger.debug(f"Updating entity with permalink: {permalink}") # Get existing entity - entity = await self.get_by_path_id(path_id) + entity = await self.get_by_permalink(permalink) if not entity: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") try: # Build update data @@ -128,13 +128,13 @@ class EntityService(BaseService[EntityModel]): logger.error(f"Failed to update entity: {e}") raise - async def delete_entity(self, path_id: str) -> bool: + async def delete_entity(self, permalink: str) -> bool: """Delete entity and its file.""" - logger.debug(f"Deleting entity: {path_id}") + logger.debug(f"Deleting entity: {permalink}") try: # Get entity first for file deletion - entity = await self.get_by_path_id(path_id) + entity = await self.get_by_permalink(permalink) # Delete file first (it's source of truth) await self.file_service.delete_entity_file(entity) @@ -143,30 +143,30 @@ class EntityService(BaseService[EntityModel]): return await self.repository.delete(entity.id) except EntityNotFoundError: - logger.info(f"Entity not found: {path_id}") + logger.info(f"Entity not found: {permalink}") return True # Already deleted except Exception as e: logger.error(f"Failed to delete entity: {e}") raise - async def delete_entities(self, path_ids: List[str]) -> bool: + async def delete_entities(self, permalinks: List[str]) -> bool: """Delete multiple entities and their files.""" - logger.debug(f"Deleting entities: {path_ids}") + logger.debug(f"Deleting entities: {permalinks}") success = True - for path_id in path_ids: - await self.delete_entity(path_id) + for permalink in permalinks: + await self.delete_entity(permalink) success = True return success - async def get_by_path_id(self, path_id: str) -> EntityModel: + async def get_by_permalink(self, permalink: str) -> EntityModel: """Get entity by type and name combination.""" - logger.debug(f"Getting entity by path_id: {path_id}") - db_entity = await self.repository.get_by_path_id(path_id) + logger.debug(f"Getting entity by permalink: {permalink}") + db_entity = await self.repository.get_by_permalink(permalink) if not db_entity: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") return db_entity async def get_all(self) -> Sequence[EntityModel]: @@ -188,10 +188,10 @@ class EntityService(BaseService[EntityModel]): logger.debug(f"Listing entities: type={entity_type} sort={sort_by}") return await self.repository.list_entities(entity_type=entity_type, sort_by=sort_by) - async def open_nodes(self, path_ids: List[str]) -> Sequence[EntityModel]: + async def open_nodes(self, permalinks: List[str]) -> Sequence[EntityModel]: """Get specific nodes and their relationships.""" - logger.debug(f"Opening nodes path_ids: {path_ids}") - return await self.repository.find_by_path_ids(path_ids) + logger.debug(f"Opening nodes permalinks: {permalinks}") + return await self.repository.find_by_permalinks(permalinks) async def delete_entity_by_file_path(self, file_path): await self.repository.delete_by_file_path(file_path) diff --git a/src/basic_memory/services/file_service.py b/src/basic_memory/services/file_service.py index e865b85e..d3622ae2 100644 --- a/src/basic_memory/services/file_service.py +++ b/src/basic_memory/services/file_service.py @@ -1,6 +1,5 @@ """Service for file operations with checksum tracking.""" -from datetime import datetime, UTC from pathlib import Path from typing import Optional, Dict, Any, Tuple @@ -31,12 +30,11 @@ class FileService: self.base_path = base_path self.knowledge_writer = knowledge_writer - def get_entity_path(self, entity: EntityModel) -> Path: """Generate filesystem path for entity.""" if entity.file_path: return self.base_path / entity.file_path - return self.base_path / f"{entity.path_id}.md" + return self.base_path / f"{entity.permalink}.md" async def write_entity_file( self, @@ -62,12 +60,11 @@ class FileService: logger.error(f"Failed to write entity file: {e}") raise FileOperationError(f"Failed to write entity file: {e}") - async def read_entity_content(self, entity: EntityModel) -> str: """Get entity's content if it's a note. Args: - path_id: Entity's path ID + permalink: Entity's path ID Returns: content without frontmatter @@ -75,7 +72,7 @@ class FileService: Raises: FileOperationError: If entity file doesn't exist """ - logger.debug(f"Reading entity with path_id: {entity.path_id}") + logger.debug(f"Reading entity with permalink: {entity.permalink}") # For notes, read the actual file content file_path = self.get_entity_path(entity) @@ -86,7 +83,6 @@ class FileService: content = content.strip() return content - async def delete_entity_file(self, entity: EntityModel) -> None: """Delete entity file from filesystem.""" try: @@ -96,7 +92,6 @@ class FileService: logger.error(f"Failed to delete entity file: {e}") raise FileOperationError(f"Failed to delete entity file: {e}") - async def exists(self, path: Path) -> bool: """ Check if file exists. @@ -265,7 +260,7 @@ class FileService: ) -> str: """ Add YAML frontmatter to content. - + Args: content: Content to add frontmatter to frontmatter: Frontmatter to add diff --git a/src/basic_memory/services/observation_service.py b/src/basic_memory/services/observation_service.py index 95dcf2ad..b61717f4 100644 --- a/src/basic_memory/services/observation_service.py +++ b/src/basic_memory/services/observation_service.py @@ -32,7 +32,7 @@ class ObservationService(BaseService[ObservationRepository]): self.file_operations = file_service async def add_observations( - self, path_id: str, observations: List[ObservationCreate], context: str | None = None + self, permalink: str, observations: List[ObservationCreate], context: str | None = None ) -> EntityModel: """Add observations to entity and update its file. @@ -41,17 +41,17 @@ class ObservationService(BaseService[ObservationRepository]): - [category] Content text #tag1 #tag2 (optional context) Args: - path_id: Entity path ID + permalink: Entity path ID observations: List of observations with categories context: Optional shared context for all observations """ - logger.debug(f"Adding observations to entity: {path_id}") + logger.debug(f"Adding observations to entity: {permalink}") try: # Get entity to update - entity = await self.entity_repository.get_by_path_id(path_id) + entity = await self.entity_repository.get_by_permalink(permalink) if not entity: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") # Add observations to DB await self.repository.create_all( @@ -68,33 +68,33 @@ class ObservationService(BaseService[ObservationRepository]): ) # Get updated entity - entity = await self.entity_repository.get_by_path_id(path_id) + entity = await self.entity_repository.get_by_permalink(permalink) # Write updated file and checksum _, checksum = await self.file_operations.write_entity_file(entity) await self.entity_repository.update(entity.id, {"checksum": checksum}) # Return final entity with all updates and relations - return await self.entity_repository.get_by_path_id(path_id) + return await self.entity_repository.get_by_permalink(permalink) except Exception as e: logger.error(f"Failed to add observations: {e}") raise - async def delete_observations(self, path_id: str, observations: List[str]) -> EntityModel: + async def delete_observations(self, permalink: str, observations: List[str]) -> EntityModel: """Delete observations from entity and update its file. Args: - path_id: Entity path ID + permalink: Entity path ID observations: List of observation contents to delete """ - logger.debug(f"Deleting observations from entity {path_id}") + logger.debug(f"Deleting observations from entity {permalink}") try: # Get entity - entity = await self.entity_repository.get_by_path_id(path_id) + entity = await self.entity_repository.get_by_permalink(permalink) if not entity: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") # Delete observations from DB by comparing the string value to the Observation content for observation in observations: @@ -107,7 +107,7 @@ class ObservationService(BaseService[ObservationRepository]): await self.entity_repository.update(entity.id, {"checksum": checksum}) # Return final entity with all updates - return await self.entity_repository.get_by_path_id(path_id) + return await self.entity_repository.get_by_permalink(permalink) except Exception as e: logger.error(f"Failed to delete observations: {e}") diff --git a/src/basic_memory/services/relation_service.py b/src/basic_memory/services/relation_service.py index 3b704bca..1b02f98f 100644 --- a/src/basic_memory/services/relation_service.py +++ b/src/basic_memory/services/relation_service.py @@ -37,8 +37,8 @@ class RelationService(BaseService[RelationRepository]): for rs in relations: try: - from_entity = await self.entity_repository.get_by_path_id(rs.from_id) - to_entity = await self.entity_repository.get_by_path_id(rs.to_id) + from_entity = await self.entity_repository.get_by_permalink(rs.from_id) + to_entity = await self.entity_repository.get_by_permalink(rs.to_id) relation = RelationModel( from_id=from_entity.id, @@ -58,10 +58,10 @@ class RelationService(BaseService[RelationRepository]): continue # Get fresh copies of all updated entities - for path_id in entities_to_update: + for permalink in entities_to_update: try: # Get fresh entity - entity = await self.entity_repository.get_by_path_id(path_id) + entity = await self.entity_repository.get_by_permalink(permalink) # Write updated file _, checksum = await self.file_service.write_entity_file(entity) @@ -70,11 +70,13 @@ class RelationService(BaseService[RelationRepository]): updated_entities.append(updated) except Exception as e: - logger.error(f"Failed to update entity {path_id}: {e}") + logger.error(f"Failed to update entity {permalink}: {e}") continue # select again to eagerly load all relations - return await self.entity_repository.find_by_path_ids([e.path_id for e in updated_entities]) + return await self.entity_repository.find_by_permalinks( + [e.permalink for e in updated_entities] + ) async def delete_relations(self, to_delete: List[RelationSchema]) -> Sequence[EntityModel]: """Delete relations and return all updated entities.""" @@ -104,12 +106,12 @@ class RelationService(BaseService[RelationRepository]): logger.warning("No relations were deleted") # Get fresh copies of all updated entities - for path_id in entities_to_update: + for permalink in entities_to_update: try: # Get fresh entity - entity = await self.entity_repository.get_by_path_id(path_id) + entity = await self.entity_repository.get_by_permalink(permalink) if not entity: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") # Write updated file _, checksum = await self.file_service.write_entity_file(entity) @@ -118,7 +120,7 @@ class RelationService(BaseService[RelationRepository]): updated_entities.append(updated) except Exception as e: - logger.error(f"Failed to update entity {path_id}: {e}") + logger.error(f"Failed to update entity {permalink}: {e}") continue return updated_entities @@ -128,9 +130,9 @@ class RelationService(BaseService[RelationRepository]): raise async def find_relation( - self, from_path_id: str, to_path_id: str, relation_type: str + self, from_permalink: str, to_permalink: str, relation_type: str ) -> RelationModel: - return await self.repository.find_relation(from_path_id, to_path_id, relation_type) + return await self.repository.find_relation(from_permalink, to_permalink, relation_type) async def delete_relation( self, from_entity: EntityModel, to_entity: EntityModel, relation_type: str diff --git a/src/basic_memory/services/search_service.py b/src/basic_memory/services/search_service.py index 79fdb715..047fbeb8 100644 --- a/src/basic_memory/services/search_service.py +++ b/src/basic_memory/services/search_service.py @@ -60,7 +60,7 @@ class SearchService: *[f"{obs.category}: {obs.content}" for obs in entity.observations], # Add relations *[ - f"{rel.relation_type} {rel.to_entity.path_id}: {rel.context or ''}" + f"{rel.relation_type} {rel.to_entity.permalink}: {rel.context or ''}" for rel in entity.relations ], ] @@ -77,7 +77,7 @@ class SearchService: background_tasks.add_task( self._do_index, content=content, - path_id=entity.path_id, + permalink=entity.permalink, file_path=entity.file_path, type=SearchItemType.ENTITY, metadata=metadata, @@ -85,20 +85,20 @@ class SearchService: else: await self._do_index( content=content, - path_id=entity.path_id, + permalink=entity.permalink, file_path=entity.file_path, type=SearchItemType.ENTITY, metadata=metadata, ) async def _do_index( - self, content: str, path_id: str, file_path: str, type: SearchItemType, metadata: dict + self, content: str, permalink: str, file_path: str, type: SearchItemType, metadata: dict ) -> None: """Actually perform the indexing.""" await self.repository.index_item( - content=content, path_id=path_id, file_path=file_path, type=type, metadata=metadata + content=content, permalink=permalink, file_path=file_path, type=type, metadata=metadata ) - async def delete_by_path_id(self, path_id: str): + async def delete_by_permalink(self, permalink: str): """Delete an item from the search index.""" - await self.repository.delete_by_path_id(path_id) + await self.repository.delete_by_permalink(permalink) diff --git a/src/basic_memory/sync/entity_sync_service.py b/src/basic_memory/sync/entity_sync_service.py index 056bd3af..fdf2f0c3 100644 --- a/src/basic_memory/sync/entity_sync_service.py +++ b/src/basic_memory/sync/entity_sync_service.py @@ -25,7 +25,7 @@ def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> Enti model = EntityModel( title=markdown.frontmatter.title, entity_type=markdown.frontmatter.type, - path_id=markdown.frontmatter.id, + permalink=markdown.frontmatter.id, file_path=file_path, content_type="text/markdown", summary=markdown.content.content, @@ -65,21 +65,21 @@ class EntitySyncService: model = entity_model_from_markdown(file_path, markdown) # Mark as incomplete sync - model.checksum = None + model.checksum = None return await self.entity_repository.add(model) async def update_entity_and_observations( - self, path_id: str, markdown: EntityMarkdown + self, permalink: str, markdown: EntityMarkdown ) -> EntityModel: """First pass: Update entity fields and observations. Updates everything except relations and sets null checksum to indicate sync not complete. """ - logger.debug(f"Updating entity and observations: {path_id}") - db_entity = await self.entity_repository.get_by_path_id(path_id) + logger.debug(f"Updating entity and observations: {permalink}") + db_entity = await self.entity_repository.get_by_permalink(permalink) if not db_entity: - raise EntityNotFoundError(f"Entity not found: {path_id}") + raise EntityNotFoundError(f"Entity not found: {permalink}") # Update fields from markdown db_entity.title = markdown.frontmatter.title @@ -88,7 +88,7 @@ class EntitySyncService: # Clear observations for entity await self.observation_repository.delete_by_fields(entity_id=db_entity.id) - + # add new observations observations = [ Observation( @@ -122,14 +122,14 @@ class EntitySyncService: checksum: Final checksum to set after relations are updated """ logger.debug(f"Updating relations for entity: {markdown.frontmatter.id}") - db_entity = await self.entity_repository.get_by_path_id(markdown.frontmatter.id) + db_entity = await self.entity_repository.get_by_permalink(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_repository.find_by_path_ids(target_entity_path_ids) + target_entity_permalinks = [rel.target for rel in markdown.content.relations] + target_entities = await self.entity_repository.find_by_permalinks(target_entity_permalinks) # dict by path - entity_by_path = {e.path_id: e for e in target_entities} + entity_by_path = {e.permalink: e for e in target_entities} # Clear and update relations await self.relation_repository.delete_outgoing_relations_from_entity(db_entity.id) diff --git a/src/basic_memory/sync/file_change_scanner.py b/src/basic_memory/sync/file_change_scanner.py index 6445a1f4..9bd90904 100644 --- a/src/basic_memory/sync/file_change_scanner.py +++ b/src/basic_memory/sync/file_change_scanner.py @@ -1,7 +1,8 @@ """Service for detecting changes between filesystem and database.""" + from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, Sequence, Any +from typing import Dict, Sequence from loguru import logger @@ -10,22 +11,24 @@ from basic_memory.repository.entity_repository import EntityRepository from basic_memory.sync.utils import SyncReport from basic_memory.utils.file_utils import compute_checksum + @dataclass class FileState: - """State of a file including file path, path_id and checksum info.""" + """State of a file including file path, permalink and checksum info.""" + file_path: str - path_id: str + permalink: str checksum: str -@dataclass +@dataclass class ScanResult: """Result of scanning a directory.""" - + # file_path -> checksum files: Dict[str, str] = field(default_factory=dict) # file_path -> error message - errors: Dict[str, str] = field(default_factory=dict) + errors: Dict[str, str] = field(default_factory=dict) class FileChangeScanner: @@ -34,9 +37,7 @@ class FileChangeScanner: The filesystem is treated as the source of truth. """ - def __init__( - self, entity_repository: EntityRepository - ): + def __init__(self, entity_repository: EntityRepository): self.entity_repository = entity_repository async def scan_directory(self, directory: Path) -> ScanResult: @@ -70,7 +71,7 @@ class FileChangeScanner: checksum = await compute_checksum(content) if checksum: # Only store valid checksums - result.files[rel_path] = checksum + result.files[rel_path] = checksum else: result.errors[rel_path] = "Failed to compute checksum" @@ -85,13 +86,15 @@ class FileChangeScanner: return result - async def find_changes(self, directory: Path, db_file_state: Dict[str, FileState]) -> SyncReport: + async def find_changes( + self, directory: Path, db_file_state: Dict[str, FileState] + ) -> SyncReport: """ Find changes between filesystem and database. Args: directory: Directory to check - db_file_state: dict mapping file_path to DbState(path_id, checksum) + db_file_state: dict mapping file_path to DbState(permalink, checksum) Returns: SyncReport detailing changes @@ -102,11 +105,11 @@ class FileChangeScanner: # Build report report = SyncReport() - + # Find new and modified files for file_path, checksum in current_files.items(): logger.debug(f"{file_path} ({checksum[:8]})") - + if file_path not in db_file_state: report.new.add(file_path) elif checksum != db_file_state[file_path].checksum: @@ -134,9 +137,7 @@ class FileChangeScanner: return report - async def get_db_file_state( - self, db_records: Sequence[Entity] - ) -> Dict[str, FileState]: + async def get_db_file_state(self, db_records: Sequence[Entity]) -> Dict[str, FileState]: """Get file_path and checksums from database. Args: db_records: database records @@ -144,7 +145,12 @@ class FileChangeScanner: Dict mapping file paths to FileState :param db_records: the data from the db """ - return {r.file_path: FileState(file_path=r.file_path, path_id=r.path_id, checksum=r.checksum) for r in db_records} + return { + r.file_path: FileState( + file_path=r.file_path, permalink=r.permalink, checksum=r.checksum + ) + for r in db_records + } async def find_knowledge_changes(self, directory: Path) -> SyncReport: """Find changes in knowledge directory.""" diff --git a/src/basic_memory/sync/sync_service.py b/src/basic_memory/sync/sync_service.py index 593932c5..68c1c1a4 100644 --- a/src/basic_memory/sync/sync_service.py +++ b/src/basic_memory/sync/sync_service.py @@ -56,10 +56,10 @@ class SyncService: file_path, entity_markdown ) else: - path_id = entity_markdown.frontmatter.id - logger.debug(f"Updating entity_markdown: {path_id}") + permalink = entity_markdown.frontmatter.id + logger.debug(f"Updating entity_markdown: {permalink}") await self.knowledge_sync_service.update_entity_and_observations( - path_id, entity_markdown + permalink, entity_markdown ) # Second pass: Process relations diff --git a/tests/api/test_discovery_router.py b/tests/api/test_discovery_router.py index cb9c3d8c..367ff27a 100644 --- a/tests/api/test_discovery_router.py +++ b/tests/api/test_discovery_router.py @@ -20,7 +20,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="Core memory service", - path_id="component/memory_service", + permalink="component/memory_service", file_path="component/memory_service.md", observations=[ Observation(category="tech", content="Using SQLite for storage"), @@ -32,7 +32,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="File format spec", - path_id="spec/file_format", + permalink="spec/file_format", file_path="spec/file_format.md", observations=[ Observation(category="feature", content="Support for frontmatter"), @@ -44,7 +44,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="Architecture decision", - path_id="decision/tech_choice", + permalink="decision/tech_choice", file_path="decision/tech_choice.md", observations=[ Observation(category="note", content="Team discussed options"), @@ -57,7 +57,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="API layer", - path_id="component/api_service", + permalink="component/api_service", file_path="component/api_service.md", observations=[ Observation(category="tech", content="FastAPI based"), @@ -143,12 +143,12 @@ async def test_list_entities_with_sorting(client: AsyncClient, test_entities): 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") + # Sort by permalink + response = await client.get("/discovery/entities/technical_component?sort_by=permalink") 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) + permalinks = [e.permalink for e in data.entities] + assert permalinks == sorted(permalinks) async def test_list_entities_empty_type(client: AsyncClient, test_entities): diff --git a/tests/api/test_knowledge_router.py b/tests/api/test_knowledge_router.py index 260e8ca0..bea11226 100644 --- a/tests/api/test_knowledge_router.py +++ b/tests/api/test_knowledge_router.py @@ -39,11 +39,11 @@ async def create_entity(client) -> EntityResponse: return create_response.entities[0] -async def add_observations(client, path_id: str) -> List[ObservationResponse]: +async def add_observations(client, permalink: str) -> List[ObservationResponse]: response = await client.post( "/knowledge/observations", json={ - "path_id": path_id, + "permalink": permalink, "observations": [ {"content": "First observation", "category": "tech"}, {"content": "Second observation", "category": "note"}, @@ -69,15 +69,19 @@ async def create_related_entities(client) -> List[RelationResponse]: # pyright: create_response = await client.post("/knowledge/entities", json={"entities": entities}) assert create_response.status_code == 200 created = create_response.json()["entities"] - source_path_id = "source_entity" - target_path_id = "target_entity" + source_permalink = "source_entity" + target_permalink = "target_entity" # Create relation between them response = await client.post( "/knowledge/relations", json={ "relations": [ - {"from_id": source_path_id, "to_id": target_path_id, "relation_type": "related_to"} + { + "from_id": source_permalink, + "to_id": target_permalink, + "relation_type": "related_to", + } ] }, ) @@ -94,13 +98,13 @@ async def create_related_entities(client) -> List[RelationResponse]: # pyright: assert len(source_entity.relations) == 1 source_relation = source_entity.relations[0] - assert source_relation.from_id == source_path_id - assert source_relation.to_id == target_path_id + assert source_relation.from_id == source_permalink + assert source_relation.to_id == target_permalink assert len(target_entity.relations) == 1 target_relation = target_entity.relations[0] - assert target_relation.from_id == source_path_id - assert target_relation.to_id == target_path_id + assert target_relation.from_id == source_permalink + assert target_relation.to_id == target_permalink return source_entity.relations + target_entity.relations @@ -121,15 +125,15 @@ async def test_get_entity(client: AsyncClient): data = response.json() # Now get it by path - path_id = data["entities"][0]["path_id"] - response = await client.get(f"/knowledge/entities/{path_id}") + permalink = data["entities"][0]["permalink"] + response = await client.get(f"/knowledge/entities/{permalink}") # Verify retrieval assert response.status_code == 200 entity = response.json() assert entity["title"] == "TestEntity" assert entity["entity_type"] == "test" - assert entity["path_id"] == "test_entity" + assert entity["permalink"] == "test_entity" @pytest.mark.asyncio @@ -145,12 +149,12 @@ async def test_add_observations(client: AsyncClient): data = {"title": "TestEntity", "entity_type": "test"} response = await client.post("/knowledge/entities", json={"entities": [data]}) - path_id = "test_entity" + permalink = "test_entity" # Add observations - await add_observations(client, path_id) + await add_observations(client, permalink) # Verify observations appear in entity - entity_response = await client.get(f"/knowledge/entities/{path_id}") + entity_response = await client.get(f"/knowledge/entities/{permalink}") entity = entity_response.json() assert len(entity["observations"]) == 2 @@ -168,7 +172,7 @@ async def test_open_nodes(client: AsyncClient): # Open nodes by path IDs response = await client.post( "/knowledge/nodes", - json={"path_ids": ["alpha_test"]}, + json={"permalinks": ["alpha_test"]}, ) # Verify results @@ -178,7 +182,7 @@ async def test_open_nodes(client: AsyncClient): entity = data["entities"][0] assert entity["title"] == "AlphaTest" assert entity["entity_type"] == "test" - assert entity["path_id"] == "alpha_test" + assert entity["permalink"] == "alpha_test" @pytest.mark.asyncio @@ -190,14 +194,14 @@ async def test_delete_entity(client: AsyncClient): # Test deletion response = await client.post( - "/knowledge/entities/delete", json={"path_ids": ["test/TestEntity"]} + "/knowledge/entities/delete", json={"permalinks": ["test/TestEntity"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify entity is gone - path_id = quote("test/TestEntity") - response = await client.get(f"/knowledge/entities/{path_id}") + permalink = quote("test/TestEntity") + response = await client.get(f"/knowledge/entities/{permalink}") assert response.status_code == 404 @@ -213,15 +217,15 @@ async def test_delete_entity_bulk(client: AsyncClient): # Test deletion response = await client.post( - "/knowledge/entities/delete", json={"path_ids": ["Entity1", "Entity2"]} + "/knowledge/entities/delete", json={"permalinks": ["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"{name}") - response = await client.get(f"/knowledge/entities/{path_id}") + permalink = quote(f"{name}") + response = await client.get(f"/knowledge/entities/{permalink}") assert response.status_code == 404 @@ -234,7 +238,7 @@ async def test_delete_entity_with_observations(client, observation_repository): await add_observations(client, "TestEntity") # Delete entity - response = await client.post("/knowledge/entities/delete", json={"path_ids": ["TestEntity"]}) + response = await client.post("/knowledge/entities/delete", json={"permalinks": ["TestEntity"]}) assert response.status_code == 200 assert response.json() == {"deleted": True} @@ -252,7 +256,7 @@ async def test_delete_observations(client, observation_repository): observations = await add_observations(client, "TestEntity") # adds 2 # Delete specific observations - request_data = {"path_id": "TestEntity", "observations": [observations[0].content]} + request_data = {"permalink": "TestEntity", "observations": [observations[0].content]} response = await client.post("/knowledge/observations/delete", json=request_data) assert response.status_code == 200 data = response.json() @@ -291,7 +295,9 @@ async def test_delete_relations(client, relation_repository): @pytest.mark.asyncio async def test_delete_nonexistent_entity(client: AsyncClient): """Test deleting a nonexistent entity by path ID.""" - response = await client.post("/knowledge/entities/delete", json={"path_ids": ["non_existent"]}) + response = await client.post( + "/knowledge/entities/delete", json={"permalinks": ["non_existent"]} + ) assert response.status_code == 200 assert response.json() == {"deleted": True} @@ -303,7 +309,7 @@ async def test_delete_nonexistent_observations(client: AsyncClient): entity_data = {"title": "TestEntity", "entity_type": "test"} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - request_data = {"path_id": "TestEntity", "observations": ["Nonexistent observation"]} + request_data = {"permalink": "TestEntity", "observations": ["Nonexistent observation"]} response = await client.post("/knowledge/observations/delete", json=request_data) assert response.status_code == 200 @@ -375,7 +381,7 @@ async def test_full_knowledge_flow(client: AsyncClient): await client.post( "/knowledge/observations", json={ - "path_id": "main_entity", + "permalink": "main_entity", "observations": [ {"content": "Connected to first related entity", "category": "tech"}, {"content": "Connected to second related entity", "category": "note"}, @@ -385,8 +391,8 @@ async def test_full_knowledge_flow(client: AsyncClient): ) # 5. Verify full graph structure - path_id = "MainEntity" - main_get = await client.get(f"/knowledge/entities/{path_id}") + permalink = "MainEntity" + main_get = await client.get(f"/knowledge/entities/{permalink}") main_entity = main_get.json() # Check entity structure @@ -401,14 +407,14 @@ async def test_full_knowledge_flow(client: AsyncClient): # 7. Delete main entity response = await client.post( - "/knowledge/entities/delete", json={"path_ids": ["MainEntity", "NonEntity"]} + "/knowledge/entities/delete", json={"permalinks": ["MainEntity", "NonEntity"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify deletion - path_id = "MainEntity" - response = await client.get(f"/knowledge/entities/{path_id}") + permalink = "MainEntity" + response = await client.get(f"/knowledge/entities/{permalink}") assert response.status_code == 404 @@ -432,7 +438,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 == "search_test" + assert search_result.results[0].permalink == "search_test" assert search_result.results[0].type == SearchItemType.ENTITY.value @@ -453,7 +459,7 @@ async def test_observation_update_indexing(client: AsyncClient): await client.post( "/knowledge/observations", json={ - "path_id": entity["path_id"], + "permalink": entity["permalink"], "observations": [{"content": "Unique sphinx observation", "category": "tech"}], }, ) @@ -464,7 +470,7 @@ async def test_observation_update_indexing(client: AsyncClient): ) search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 1 - assert search_result.results[0].path_id == entity["path_id"] + assert search_result.results[0].permalink == entity["permalink"] @pytest.mark.asyncio @@ -490,7 +496,7 @@ async def test_entity_delete_indexing(client: AsyncClient): # Delete entity delete_response = await client.post( - "/knowledge/entities/delete", json={"path_ids": [entity["path_id"]]} + "/knowledge/entities/delete", json={"permalinks": [entity["permalink"]]} ) assert delete_response.status_code == 200 @@ -535,8 +541,8 @@ 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 == {"source_test", "target_test"} + permalinks = {r.permalink for r in search_result.results} + assert permalinks == {"source_test", "target_test"} @pytest.mark.asyncio @@ -557,7 +563,7 @@ async def test_update_entity_basic(client: AsyncClient): "title": "updated-test", "summary": "Updated description", } - response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data) + response = await client.put(f"/knowledge/entities/{entity['permalink']}", json=update_data) assert response.status_code == 200 updated = response.json() @@ -574,25 +580,26 @@ async def test_get_entity_content_parameter(client: AsyncClient): data = { "title": "TestContent", "entity_type": "test", - "content": "# Test Content\n\nSome test content." + "content": "# Test Content\n\nSome test content.", } response = await client.post("/knowledge/entities", json={"entities": [data]}) assert response.status_code == 200 - path_id = response.json()["entities"][0]["path_id"] + permalink = response.json()["entities"][0]["permalink"] # Get without content - response = await client.get(f"/knowledge/entities/{path_id}") + response = await client.get(f"/knowledge/entities/{permalink}") assert response.status_code == 200 entity = response.json() assert entity["content"] is None # Get with content - response = await client.get(f"/knowledge/entities/{path_id}?content=true") + response = await client.get(f"/knowledge/entities/{permalink}?content=true") assert response.status_code == 200 entity = response.json() assert "# Test Content" in entity["content"] assert "Some test content" in entity["content"] + @pytest.mark.asyncio async def test_update_entity_content(client: AsyncClient): """Test updating content for different entity types.""" @@ -604,13 +611,13 @@ async def test_update_entity_content(client: AsyncClient): # Update note content new_content = "# Updated Note\n\nNew content." response = await client.put( - f"/knowledge/entities/{note['path_id']}", json={"content": new_content} + f"/knowledge/entities/{note['permalink']}", json={"content": new_content} ) assert response.status_code == 200 updated = response.json() # Verify through get request to check file - response = await client.get(f"/knowledge/entities/{updated['path_id']}?content=true") + response = await client.get(f"/knowledge/entities/{updated['permalink']}?content=true") fetched = response.json() assert "# Updated Note" in fetched["content"] assert "New content" in fetched["content"] @@ -631,7 +638,7 @@ async def test_update_entity_type_conversion(client: AsyncClient): # Convert to knowledge type response = await client.put( - f"/knowledge/entities/{note['path_id']}", json={"entity_type": "test"} + f"/knowledge/entities/{note['permalink']}", json={"entity_type": "test"} ) assert response.status_code == 200 updated = response.json() @@ -640,7 +647,7 @@ async def test_update_entity_type_conversion(client: AsyncClient): assert updated["entity_type"] == "test" # Get latest to verify file format - response = await client.get(f"/knowledge/entities/{updated['path_id']}") + response = await client.get(f"/knowledge/entities/{updated['permalink']}") knowledge = response.json() assert knowledge.get("content") is None @@ -655,7 +662,7 @@ async def test_update_entity_metadata(client: AsyncClient): # Update metadata update_data = {"entity_metadata": {"status": "final", "reviewed": True}} - response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data) + response = await client.put(f"/knowledge/entities/{entity['permalink']}", json=update_data) assert response.status_code == 200 updated = response.json() @@ -681,7 +688,7 @@ async def test_update_entity_search_index(client: AsyncClient): # Update with new searchable content update_data = {"summary": "Updated with unique sphinx marker"} - response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data) + response = await client.put(f"/knowledge/entities/{entity['permalink']}", json=update_data) assert response.status_code == 200 # Search should find new content @@ -690,7 +697,7 @@ async def test_update_entity_search_index(client: AsyncClient): ) results = search_response.json()["results"] assert len(results) == 1 - assert results[0]["path_id"] == entity["path_id"] + assert results[0]["permalink"] == entity["permalink"] @pytest.mark.asyncio @@ -699,9 +706,7 @@ async def test_get_entity_with_relations(client: AsyncClient): # Create a note and knowledge entity note = await client.post( "/knowledge/entities", - json={ - "entities": [{"title": "test-note", "entity_type": "note", "summary": "Test note"}] - }, + json={"entities": [{"title": "test-note", "entity_type": "note", "summary": "Test note"}]}, ) knowledge = await client.post( "/knowledge/entities", @@ -718,8 +723,8 @@ async def test_get_entity_with_relations(client: AsyncClient): json={ "relations": [ { - "from_id": note.json()["entities"][0]["path_id"], - "to_id": knowledge.json()["entities"][0]["path_id"], + "from_id": note.json()["entities"][0]["permalink"], + "to_id": knowledge.json()["entities"][0]["permalink"], "relation_type": "references", } ] @@ -727,9 +732,11 @@ async def test_get_entity_with_relations(client: AsyncClient): ) # Verify GET returns relations for both types - note_response = await client.get(f"/knowledge/entities/{note.json()['entities'][0]['path_id']}") + note_response = await client.get( + f"/knowledge/entities/{note.json()['entities'][0]['permalink']}" + ) knowledge_response = await client.get( - f"/knowledge/entities/{knowledge.json()['entities'][0]['path_id']}" + f"/knowledge/entities/{knowledge.json()['entities'][0]['permalink']}" ) assert len(note_response.json()["relations"]) == 1 diff --git a/tests/api/test_search_router.py b/tests/api/test_search_router.py index 8b52949c..125294f6 100644 --- a/tests/api/test_search_router.py +++ b/tests/api/test_search_router.py @@ -20,7 +20,7 @@ def test_entity(): title = "TestComponent" entity_type = "test" entity_metadata = {"test": "test"} - path_id = "component/test_component" + permalink = "component/test_component" file_path = "entities/component/test_component.md" summary = "A test component for search testing" content_type = "text/markdown" @@ -46,7 +46,7 @@ async def test_search_basic(client, indexed_entity): assert response.status_code == 200 search_results = SearchResponse.model_validate(response.json()) assert len(search_results.results) == 1 - assert search_results.results[0].path_id == indexed_entity.path_id + assert search_results.results[0].permalink == indexed_entity.permalink @pytest.mark.asyncio @@ -141,13 +141,14 @@ async def test_search_empty(search_service, client): async def test_reindex(client, search_service, entity_service, test_entity, session_maker): """Test reindex endpoint.""" # Create test entity and document - await entity_service.create_entity( EntitySchema( + await entity_service.create_entity( + EntitySchema( title="TestEntity1", entity_type="test", summary="A test entity description", observations=["this is a test observation"], ), -) + ) # Clear search index async with db.scoped_session(session_maker) as session: @@ -186,6 +187,6 @@ async def test_multiple_filters(client, indexed_entity): search_result = SearchResponse.model_validate(response.json()) assert len(search_result.results) == 1 result = search_result.results[0] - assert result.path_id == indexed_entity.path_id + assert result.permalink == indexed_entity.permalink assert result.type == SearchItemType.ENTITY.value assert result.metadata["entity_type"] == "test" diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py index e49a47bb..847b86af 100644 --- a/tests/cli/test_status.py +++ b/tests/cli/test_status.py @@ -37,7 +37,7 @@ # deleted={"old/deleted.md"}, # moved={ # "new/location.md": FileState( -# path_id="new/location.md", checksum="abc123", moved_from="old/location.md" +# permalink="new/location.md", checksum="abc123", moved_from="old/location.md" # ) # }, # ) @@ -61,7 +61,7 @@ # deleted={"old/deleted.md"}, # moved={ # "new/location.md": FileState( -# path_id="new/location.md", +# permalink="new/location.md", # checksum="abc123def", # 8 chars for display # moved_from="old/location.md", # ) @@ -100,7 +100,7 @@ # # # Add some files to DB with different paths to test moves # await entity_repository.create( -# {"path_id": "old/doc.md", "file_path": "old/doc.md", "checksum": "abc123"} +# {"permalink": "old/doc.md", "file_path": "old/doc.md", "checksum": "abc123"} # ) # # # Run status check @@ -133,7 +133,7 @@ # # # Add to DB # await entity_repository.create( -# {"path_id": original_path, "file_path": original_path, "checksum": checksum} +# {"permalink": original_path, "file_path": original_path, "checksum": checksum} # ) # # # Simulate case change in filesystem @@ -160,7 +160,7 @@ # checksum = await compute_checksum(content) # # # Add to DB with same path -# await entity_repository.create({"path_id": path, "file_path": path, "checksum": checksum}) +# await entity_repository.create({"permalink": path, "file_path": path, "checksum": checksum}) # # # Check changes # changes = await file_change_scanner.find_knowledge_changes(docs_dir) diff --git a/tests/conftest.py b/tests/conftest.py index 4ffd1013..0ab05c91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -211,7 +211,7 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity: "title": "Test Entity", "entity_type": "test", "summary": "A test entity", - "path_id": "test/test_entity", + "permalink": "test/test_entity", "file_path": "test/test_entity.md", "content_type": "text/markdown", } diff --git a/tests/markdown/test_knowledge_writer.py b/tests/markdown/test_knowledge_writer.py index a9c41be9..baa4103c 100644 --- a/tests/markdown/test_knowledge_writer.py +++ b/tests/markdown/test_knowledge_writer.py @@ -20,7 +20,7 @@ def sample_entity() -> Entity: id=1, title="test_entity", entity_type="test", - path_id="knowledge/test_entity", + permalink="knowledge/test_entity", file_path="knowledge/test_entity.md", summary="Test description", created_at=datetime(2025, 1, 1, tzinfo=UTC), @@ -44,7 +44,7 @@ def entity_with_observations(sample_entity: Entity) -> Entity: def entity_with_relations(sample_entity: Entity) -> Entity: """Create an entity with relations.""" target = Entity( - id=2, title="target_entity", entity_type="test", path_id="knowledge/target_entity" + id=2, title="target_entity", entity_type="test", permalink="knowledge/target_entity" ) sample_entity.outgoing_relations = [ Relation(from_id=1, to_id=2, relation_type="connects_to", to_entity=target) diff --git a/tests/mcp/test_tool_add_observations.py b/tests/mcp/test_tool_add_observations.py index 362e1894..37672f57 100644 --- a/tests/mcp/test_tool_add_observations.py +++ b/tests/mcp/test_tool_add_observations.py @@ -17,11 +17,11 @@ async def test_add_basic_observation(client): # First create an entity to add observations to entity_request = CreateEntityRequest(entities=[Entity(title="TestEntity", entity_type="test")]) result = await create_entities(entity_request) - entity_id = result.entities[0].path_id + entity_id = result.entities[0].permalink # Add an observation request = AddObservationsRequest( - path_id=entity_id, observations=[ObservationCreate(content="Test observation")] + permalink=entity_id, observations=[ObservationCreate(content="Test observation")] ) updated = await add_observations(request) @@ -38,11 +38,11 @@ async def test_add_categorized_observations(client): # Create test entity entity_request = CreateEntityRequest(entities=[Entity(title="TestEntity", entity_type="test")]) result = await create_entities(entity_request) - entity_id = result.entities[0].path_id + entity_id = result.entities[0].permalink # Add observations with different categories request = AddObservationsRequest( - path_id=entity_id, + permalink=entity_id, observations=[ ObservationCreate( content="Implementation uses SQLite", category=ObservationCategory.TECH @@ -75,12 +75,12 @@ async def test_add_observations_with_context(client): # Create test entity entity_request = CreateEntityRequest(entities=[Entity(title="TestEntity", entity_type="test")]) result = await create_entities(entity_request) - entity_id = result.entities[0].path_id + entity_id = result.entities[0].permalink # Add observations with context shared_context = "Design meeting 2024-12-25" request = AddObservationsRequest( - path_id=entity_id, + permalink=entity_id, context=shared_context, observations=[ ObservationCreate( @@ -108,11 +108,11 @@ async def test_add_observations_preserves_existing(client): ] ) result = await create_entities(entity_request) - entity_id = result.entities[0].path_id + entity_id = result.entities[0].permalink # Add new observations request = AddObservationsRequest( - path_id=entity_id, + permalink=entity_id, observations=[ ObservationCreate(content="New observation", category=ObservationCategory.TECH) ], @@ -132,13 +132,13 @@ async def test_add_multiple_observations_same_category(client): # Create test entity entity_request = CreateEntityRequest(entities=[Entity(title="TestEntity", entity_type="test")]) result = await create_entities(entity_request) - entity_id = result.entities[0].path_id + entity_id = result.entities[0].permalink # Add multiple tech observations tech_observations = ["Uses async/await", "Implements SQLite backend", "Handles UTF-8 encoding"] request = AddObservationsRequest( - path_id=entity_id, + permalink=entity_id, observations=[ ObservationCreate(content=obs, category=ObservationCategory.TECH) for obs in tech_observations @@ -158,7 +158,7 @@ async def test_add_observation_to_nonexistent_entity(client): """Test adding observations to a non-existent entity fails.""" # Create request for non-existent entity request = AddObservationsRequest( - path_id="test/nonexistent", + permalink="test/nonexistent", observations=[ ObservationCreate(content="This should fail", category=ObservationCategory.NOTE) ], diff --git a/tests/mcp/test_tool_create_entities.py b/tests/mcp/test_tool_create_entities.py index 9eefbc89..10c5473f 100644 --- a/tests/mcp/test_tool_create_entities.py +++ b/tests/mcp/test_tool_create_entities.py @@ -30,7 +30,7 @@ async def test_create_basic_entity(client): entity = result.entities[0] assert entity.title == "TestEntity" assert entity.entity_type == "test" - assert entity.path_id == "test_entity" + assert entity.permalink == "test_entity" assert entity.summary == "A test entity" # Check observations @@ -122,7 +122,7 @@ async def test_create_minimal_entity(client): entity = result.entities[0] assert entity.title == "MinimalEntity" assert entity.entity_type == "test" - assert entity.path_id == "minimal_entity" + assert entity.permalink == "minimal_entity" assert entity.summary is None assert len(entity.observations) == 0 assert len(entity.relations) == 0 diff --git a/tests/mcp/test_tool_create_relations.py b/tests/mcp/test_tool_create_relations.py index 8cd37f02..5cb31146 100644 --- a/tests/mcp/test_tool_create_relations.py +++ b/tests/mcp/test_tool_create_relations.py @@ -30,8 +30,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 == "source_entity") - target = next(e for e in result.entities if e.path_id == "target_entity") + source = next(e for e in result.entities if e.permalink == "source_entity") + target = next(e for e in result.entities if e.permalink == "target_entity") # Both entities should have the relation for bi-directional navigation assert len(source.relations) == 1 @@ -74,8 +74,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 == "source") - target = next(e for e in result.entities if e.path_id == "target") + source = next(e for e in result.entities if e.permalink == "source") + target = next(e for e in result.entities if e.permalink == "target") # Both entities should have the relation with context assert len(source.relations) == 1 @@ -109,9 +109,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 == "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 = next(e for e in result.entities if e.permalink == "entity1") + entity2 = next(e for e in result.entities if e.permalink == "entity2") + entity3 = next(e for e in result.entities if e.permalink == "entity3") # Entity1 and Entity2 should share the connects_to relation assert len(entity1.relations) == 1 @@ -146,8 +146,8 @@ async def test_create_bidirectional_relations(client): ) result = await create_relations(relation_request) - 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") + service = next(e for e in result.entities if e.permalink == "service") + database = next(e for e in result.entities if e.permalink == "database") # Each entity should have both relations for full navigation assert len(service.relations) == 2 diff --git a/tests/mcp/test_tool_discovery.py b/tests/mcp/test_tool_discovery.py index 46b3cb73..5d5c3ee6 100644 --- a/tests/mcp/test_tool_discovery.py +++ b/tests/mcp/test_tool_discovery.py @@ -35,7 +35,7 @@ async def test_get_observation_categories(client): ] await add_observations( - AddObservationsRequest(path_id=entity.path_id, observations=observations) + AddObservationsRequest(permalink=entity.permalink, observations=observations) ) # Get categories diff --git a/tests/mcp/test_tool_get_entity.py b/tests/mcp/test_tool_get_entity.py index 00141e59..1042925e 100644 --- a/tests/mcp/test_tool_get_entity.py +++ b/tests/mcp/test_tool_get_entity.py @@ -23,15 +23,15 @@ async def test_get_basic_entity(client): ] ) create_result = await create_entities(entity_request) - path_id = create_result.entities[0].path_id + permalink = create_result.entities[0].permalink # Get the entity without content - entity = await get_entity(path_id) + entity = await get_entity(permalink) # Verify entity details assert entity.title == "TestEntity" assert entity.entity_type == "test" - assert entity.path_id == "test_entity" + assert entity.permalink == "test_entity" assert entity.summary == "A test entity" # Check observations @@ -40,6 +40,7 @@ async def test_get_basic_entity(client): assert obs.content == "First observation" assert obs.category == ObservationCategory.NOTE + @pytest.mark.asyncio async def test_get_entity_with_content(client): """Test retrieving a basic entity.""" @@ -55,10 +56,10 @@ async def test_get_entity_with_content(client): ] ) create_result = await create_entities(entity_request) - path_id = create_result.entities[0].path_id + permalink = create_result.entities[0].permalink # Get entity with content - entity = await get_entity(path_id, content=True) + entity = await get_entity(permalink, content=True) assert entity.content is not None # if we passed in content, it should just be the @@ -113,14 +114,14 @@ async def test_get_entity_with_categorized_observations(client): ] ) result = await create_entities(entity_request) - path_id = result.entities[0].path_id + permalink = result.entities[0].permalink # Add observations with different categories from basic_memory.mcp.tools.knowledge import add_observations from basic_memory.schemas.request import AddObservationsRequest, ObservationCreate obs_request = AddObservationsRequest( - path_id=path_id, + permalink=permalink, observations=[ ObservationCreate(content="Technical detail", category=ObservationCategory.TECH), ObservationCreate(content="Design decision", category=ObservationCategory.DESIGN), @@ -130,7 +131,7 @@ async def test_get_entity_with_categorized_observations(client): await add_observations(obs_request) # Get and verify entity without content - entity = await get_entity(path_id) + entity = await get_entity(permalink) assert len(entity.observations) == 3 categories = {obs.category for obs in entity.observations} assert ObservationCategory.TECH in categories diff --git a/tests/mcp/test_tool_open_nodes.py b/tests/mcp/test_tool_open_nodes.py index 7e9b5655..952d3d0b 100644 --- a/tests/mcp/test_tool_open_nodes.py +++ b/tests/mcp/test_tool_open_nodes.py @@ -20,10 +20,10 @@ async def test_open_multiple_entities(client): ] ) create_result = await create_entities(entity_request) - path_ids = [e.path_id for e in create_result.entities] + permalinks = [e.permalink for e in create_result.entities] # Open the nodes - request = OpenNodesRequest(path_ids=path_ids) + request = OpenNodesRequest(permalinks=permalinks) result = await open_nodes(request) assert isinstance(result, EntityListResponse) response = EntityListResponse.model_validate(result) @@ -32,7 +32,7 @@ async def test_open_multiple_entities(client): assert len(response.entities) == 2 for response_entity in response.entities: - assert response_entity.path_id in path_ids + assert response_entity.permalink in permalinks assert response_entity.title in ["Entity1", "Entity2"] @@ -51,10 +51,10 @@ async def test_open_nodes_with_details(client): ] ) create_result = await create_entities(entity_request) - path_id = create_result.entities[0].path_id + permalink = create_result.entities[0].permalink # Open the node - request = OpenNodesRequest(path_ids=[path_id]) + request = OpenNodesRequest(permalinks=[permalink]) result = await open_nodes(request) response = EntityListResponse.model_validate(result) @@ -77,7 +77,7 @@ async def test_open_nodes_with_relations(client): ] ) create_result = await create_entities(entity_request) - path_ids = [e.path_id for e in create_result.entities] + permalinks = [e.permalink for e in create_result.entities] # Add a relation between them from basic_memory.mcp.tools.knowledge import create_relations @@ -85,12 +85,12 @@ async def test_open_nodes_with_relations(client): from basic_memory.schemas.base import Relation relation_request = CreateRelationsRequest( - relations=[Relation(from_id=path_ids[0], to_id=path_ids[1], relation_type="depends_on")] + relations=[Relation(from_id=permalinks[0], to_id=permalinks[1], relation_type="depends_on")] ) await create_relations(relation_request) # Open both nodes - request = OpenNodesRequest(path_ids=path_ids) + request = OpenNodesRequest(permalinks=permalinks) result = await open_nodes(request) response = EntityListResponse.model_validate(result) @@ -105,31 +105,33 @@ async def test_open_nonexistent_nodes(client): # First create one real entity entity_request = CreateEntityRequest(entities=[Entity(title="RealEntity", entity_type="test")]) create_result = await create_entities(entity_request) - real_path_id = create_result.entities[0].path_id + real_permalink = create_result.entities[0].permalink # Try to open both real and non-existent - request = OpenNodesRequest(path_ids=[real_path_id, "nonexistent"]) + request = OpenNodesRequest(permalinks=[real_permalink, "nonexistent"]) result = await open_nodes(request) response = EntityListResponse.model_validate(result) # Should only get the real entity back assert len(response.entities) == 1 - assert real_path_id in response.entities[0].path_id + assert real_permalink in response.entities[0].permalink @pytest.mark.asyncio async def test_open_single_node(client): - """Test behavior with single path_id.""" + """Test behavior with single permalink.""" # Create an entity - entity_request = CreateEntityRequest(entities=[Entity(title="SingleEntity", entity_type="test")]) + entity_request = CreateEntityRequest( + entities=[Entity(title="SingleEntity", entity_type="test")] + ) create_result = await create_entities(entity_request) - path_id = create_result.entities[0].path_id + permalink = create_result.entities[0].permalink # Open just one node - request = OpenNodesRequest(path_ids=[path_id]) + request = OpenNodesRequest(permalinks=[permalink]) result = await open_nodes(request) response = EntityListResponse.model_validate(result) # Should get just that entity assert len(response.entities) == 1 - assert path_id in response.entities[0].path_id + assert permalink in response.entities[0].permalink diff --git a/tests/repository/test_entity_repository.py b/tests/repository/test_entity_repository.py index ca989b78..6b44a925 100644 --- a/tests/repository/test_entity_repository.py +++ b/tests/repository/test_entity_repository.py @@ -30,7 +30,7 @@ async def related_entities(session_maker): source = Entity( title="source", entity_type="test", - path_id="source/source", + permalink="source/source", file_path="source/source.md", summary="Source entity", content_type="text/markdown", @@ -38,7 +38,7 @@ async def related_entities(session_maker): target = Entity( title="target", entity_type="test", - path_id="target/target", + permalink="target/target", file_path="target/target.md", summary="Target entity", content_type="text/markdown", @@ -59,7 +59,7 @@ async def test_create_entity(entity_repository: EntityRepository): entity_data = { "title": "Test", "entity_type": "test", - "path_id": "test/test", + "permalink": "test/test", "file_path": "test/test.md", "summary": "Test description", "content_type": "text/markdown", @@ -93,7 +93,7 @@ async def test_create_all(entity_repository: EntityRepository): { "title": "Test_1", "entity_type": "test", - "path_id": "test/test_1", + "permalink": "test/test_1", "file_path": "test/test_1.md", "summary": "Test description", "content_type": "text/markdown", @@ -101,7 +101,7 @@ async def test_create_all(entity_repository: EntityRepository): { "title": "Test-2", "entity_type": "test", - "path_id": "test/test_2", + "permalink": "test/test_2", "file_path": "test/test_2.md", "summary": "Test description", "content_type": "text/markdown", @@ -131,7 +131,7 @@ async def test_create_entity_null_description(session_maker, entity_repository: entity_data = { "title": "Test", "entity_type": "test", - "path_id": "test/test", + "permalink": "test/test", "file_path": "test/test.md", "content_type": "text/markdown", "summary": None, @@ -167,9 +167,7 @@ async def test_find_by_id(entity_repository: EntityRepository, sample_entity: En @pytest.mark.asyncio async def test_update_entity(entity_repository: EntityRepository, sample_entity: Entity): """Test updating an entity""" - updated = await entity_repository.update( - sample_entity.id, {"summary": "Updated description"} - ) + updated = await entity_repository.update(sample_entity.id, {"summary": "Updated description"}) assert updated is not None assert updated.summary == "Updated description" assert updated.title == sample_entity.title # Other fields unchanged @@ -282,15 +280,15 @@ async def test_entities(session_maker): title="entity1", entity_type="test", summary="First test entity", - path_id="type1/entity1", + permalink="type1/entity1", file_path="type1/entity1.md", - content_type= "text/markdown", + content_type="text/markdown", ), Entity( title="entity2", entity_type="test", summary="Second test entity", - path_id="type1/entity2", + permalink="type1/entity2", file_path="type1/entity2.md", content_type="text/markdown", ), @@ -298,7 +296,7 @@ async def test_entities(session_maker): title="entity3", entity_type="test", summary="Third test entity", - path_id="type2/entity3", + permalink="type2/entity3", file_path="type2/entity3.md", content_type="text/markdown", ), @@ -308,39 +306,39 @@ async def test_entities(session_maker): @pytest.mark.asyncio -async def test_find_by_path_ids(entity_repository: EntityRepository, test_entities): +async def test_find_by_permalinks(entity_repository: EntityRepository, test_entities): """Test finding multiple entities by their type/name pairs.""" # Test finding multiple entities - path_ids = [e.path_id for e in test_entities] - found = await entity_repository.find_by_path_ids(path_ids) + permalinks = [e.permalink for e in test_entities] + found = await entity_repository.find_by_permalinks(permalinks) assert len(found) == 3 names = {e.title for e in found} assert names == {"entity1", "entity2", "entity3"} # Test finding subset of entities - path_ids = [e.path_id for e in test_entities if e.title != "entity2"] - found = await entity_repository.find_by_path_ids(path_ids) + permalinks = [e.permalink for e in test_entities if e.title != "entity2"] + found = await entity_repository.find_by_permalinks(permalinks) assert len(found) == 2 names = {e.title for e in found} assert names == {"entity1", "entity3"} # Test with non-existent entities - path_ids = ["type1/entity1", "type3/nonexistent"] - found = await entity_repository.find_by_path_ids(path_ids) + permalinks = ["type1/entity1", "type3/nonexistent"] + found = await entity_repository.find_by_permalinks(permalinks) assert len(found) == 1 assert found[0].title == "entity1" # Test empty input - found = await entity_repository.find_by_path_ids([]) + found = await entity_repository.find_by_permalinks([]) assert len(found) == 0 @pytest.mark.asyncio -async def test_delete_by_path_ids(entity_repository: EntityRepository, test_entities): +async def test_delete_by_permalinks(entity_repository: EntityRepository, test_entities): """Test deleting entities by type/name pairs.""" # Test deleting multiple entities - path_ids = [e.path_id for e in test_entities if e.title != "entity3"] - deleted_count = await entity_repository.delete_by_path_ids(path_ids) + permalinks = [e.permalink for e in test_entities if e.title != "entity3"] + deleted_count = await entity_repository.delete_by_permalinks(permalinks) assert deleted_count == 2 # Verify deletions @@ -350,16 +348,16 @@ async def test_delete_by_path_ids(entity_repository: EntityRepository, test_enti # Test deleting non-existent entities path__ids = ["type3/nonexistent"] - deleted_count = await entity_repository.delete_by_path_ids(path__ids) + deleted_count = await entity_repository.delete_by_permalinks(path__ids) assert deleted_count == 0 # Test empty input - deleted_count = await entity_repository.delete_by_path_ids([]) + deleted_count = await entity_repository.delete_by_permalinks([]) assert deleted_count == 0 @pytest.mark.asyncio -async def test_delete_by_path_ids_with_observations( +async def test_delete_by_permalinks_with_observations( entity_repository: EntityRepository, test_entities, session_maker ): """Test deleting entities with observations by type/name pairs.""" @@ -372,8 +370,8 @@ async def test_delete_by_path_ids_with_observations( session.add_all(observations) # Delete entities - path_ids = [e.path_id for e in test_entities] - deleted_count = await entity_repository.delete_by_path_ids(path_ids) + permalinks = [e.permalink for e in test_entities] + deleted_count = await entity_repository.delete_by_permalinks(permalinks) assert deleted_count == 3 # Verify observations were cascaded @@ -396,7 +394,7 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s core = Entity( title="core_service", entity_type="note", - path_id="service/core", + permalink="service/core", file_path="service/core.md", summary="Core service", content_type="text/markdown", @@ -404,7 +402,7 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s dbe = Entity( title="db_service", entity_type="test", - path_id="service/db", + permalink="service/db", file_path="service/db.md", summary="Database service", content_type="text/markdown", @@ -413,7 +411,7 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s config = Entity( title="service_config", entity_type="test", - path_id="config/service", + permalink="config/service", file_path="config/service.md", summary="Service configuration", content_type="text/markdown", diff --git a/tests/repository/test_observation_repository.py b/tests/repository/test_observation_repository.py index 06109943..60e1c2d3 100644 --- a/tests/repository/test_observation_repository.py +++ b/tests/repository/test_observation_repository.py @@ -91,7 +91,7 @@ async def test_delete_observations(session_maker: async_sessionmaker, repo): title="test_entity", entity_type="test", summary="Test entity", - path_id="test/test_entity", + permalink="test/test_entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -121,7 +121,7 @@ async def test_delete_observation_by_id(session_maker: async_sessionmaker, repo) title="test_entity", entity_type="test", summary="Test entity", - path_id="test/test_entity", + permalink="test/test_entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -150,7 +150,7 @@ async def test_delete_observation_by_content(session_maker: async_sessionmaker, title="test_entity", entity_type="test", summary="Test entity", - path_id="test/test_entity", + permalink="test/test_entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -181,7 +181,7 @@ async def test_find_by_category(session_maker: async_sessionmaker, repo): title="test_entity", entity_type="test", summary="Test entity", - path_id="test/test_entity", + permalink="test/test_entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -223,7 +223,7 @@ async def test_observation_categories(session_maker: async_sessionmaker, repo): title="test_entity", entity_type="test", summary="Test entity", - path_id="test/test_entity", + permalink="test/test_entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -271,7 +271,7 @@ async def test_find_by_category_case_sensitivity(session_maker: async_sessionmak title="test_entity", entity_type="test", summary="Test entity", - path_id="test/test_entity", + permalink="test/test_entity", file_path="test/test_entity.md", content_type="text/markdown", ) diff --git a/tests/repository/test_relation_repository.py b/tests/repository/test_relation_repository.py index b9416ed0..ec70d19d 100644 --- a/tests/repository/test_relation_repository.py +++ b/tests/repository/test_relation_repository.py @@ -15,7 +15,7 @@ async def source_entity(session_maker): entity = Entity( title="test_source", entity_type="test", - path_id="source/test_source", + permalink="source/test_source", file_path="source/test_source.md", summary="Source entity", content_type="text/markdown", @@ -32,7 +32,7 @@ async def target_entity(session_maker): entity = Entity( title="test_target", entity_type="test", - path_id="target/test_target", + permalink="target/test_target", file_path="target/test_target.md", summary="Target entity", content_type="text/markdown", @@ -62,7 +62,7 @@ async def related_entity(entity_repository): entity_data = { "title": "Related Entity", "entity_type": "test", - "path_id": "test/related_entity", + "permalink": "test/related_entity", "file_path": "test/related_entity.md", "summary": "A related test entity", "content_type": "text/markdown", @@ -165,8 +165,8 @@ async def test_find_by_entities( async def test_find_relation(relation_repository: RelationRepository, sample_relation: Relation): """Test finding relations by type""" relation = await relation_repository.find_relation( - from_path_id=sample_relation.from_entity.path_id, - to_path_id=sample_relation.to_entity.path_id, + from_permalink=sample_relation.from_entity.permalink, + to_permalink=sample_relation.to_entity.permalink, relation_type=sample_relation.relation_type, ) assert relation.id == sample_relation.id diff --git a/tests/schemas/test_activity_schemas.py b/tests/schemas/test_activity_schemas.py index ad429ced..cb6ff17f 100644 --- a/tests/schemas/test_activity_schemas.py +++ b/tests/schemas/test_activity_schemas.py @@ -42,7 +42,7 @@ def test_timeframe_to_timedelta(): assert TimeFrame("1w").to_timedelta == timedelta(weeks=1) assert TimeFrame("1m").to_timedelta == timedelta(days=30) # Approximate - + def test_activity_change_model(): """Test ActivityChange model.""" now = datetime.utcnow() @@ -50,15 +50,15 @@ def test_activity_change_model(): activity_type=ActivityType.ENTITY, change_type=ChangeType.CREATED, timestamp=now, - path_id="test/path", + permalink="test/path", summary="Created test document", - content="Test content" + content="Test content", ) - + assert change.activity_type == ActivityType.ENTITY assert change.change_type == ChangeType.CREATED assert change.timestamp == now - assert change.path_id == "test/path" + assert change.permalink == "test/path" assert change.summary == "Created test document" assert change.content == "Test content" @@ -66,11 +66,9 @@ def test_activity_change_model(): def test_activity_summary_model(): """Test ActivitySummary model.""" summary = ActivitySummary( - entity_changes=3, - relation_changes=2, - most_active_paths=["path1", "path2"] + entity_changes=3, relation_changes=2, most_active_paths=["path1", "path2"] ) - + assert summary.entity_changes == 3 assert summary.relation_changes == 2 assert summary.most_active_paths == ["path1", "path2"] @@ -83,21 +81,15 @@ def test_recent_activity_model(): activity_type=ActivityType.ENTITY, change_type=ChangeType.CREATED, timestamp=now, - path_id="test/path", - summary="Created test document" + permalink="test/path", + summary="Created test document", ) - - summary = ActivitySummary( - entity_changes=1 - ) - - activity = RecentActivity( - timeframe="1d", - changes=[change], - summary=summary - ) - + + summary = ActivitySummary(entity_changes=1) + + activity = RecentActivity(timeframe="1d", changes=[change], summary=summary) + assert activity.timeframe == "1d" assert len(activity.changes) == 1 - assert activity.changes[0].path_id == "test/path" - assert activity.summary.entity_changes == 1 \ No newline at end of file + assert activity.changes[0].permalink == "test/path" + assert activity.summary.entity_changes == 1 diff --git a/tests/schemas/test_observation_schemas.py b/tests/schemas/test_observation_schemas.py index 7b8b0c3e..71dc7356 100644 --- a/tests/schemas/test_observation_schemas.py +++ b/tests/schemas/test_observation_schemas.py @@ -41,7 +41,7 @@ def test_observation_response(): """Test ObservationResponse validation and conversion.""" data = { "id": 1, - "path_id": 1, + "permalink": 1, "content": "Test observation", "category": "tech", "context": "Test context", diff --git a/tests/schemas/test_schemas.py b/tests/schemas/test_schemas.py index cfa3c63c..61f68704 100644 --- a/tests/schemas/test_schemas.py +++ b/tests/schemas/test_schemas.py @@ -9,7 +9,8 @@ from basic_memory.schemas import ( Relation, CreateEntityRequest, SearchNodesRequest, - OpenNodesRequest, RelationResponse, + OpenNodesRequest, + RelationResponse, ) from basic_memory.schemas.base import to_snake_case @@ -70,9 +71,16 @@ def test_relation_in_validation(): with pytest.raises(ValidationError): Relation.model_validate({"from_id": "123", "to_id": "456"}) # Missing relationType + def test_relation_response(): """Test RelationResponse validation.""" - data = {"from_id": "test/123", "to_id": "test/456", "relation_type": "test", "from_entity":{"path_id": "test/123"}, "to_entity":{"path_id": "test/456"}} + data = { + "from_id": "test/123", + "to_id": "test/456", + "relation_type": "test", + "from_entity": {"permalink": "test/123"}, + "to_entity": {"permalink": "test/456"}, + } relation = RelationResponse.model_validate(data) assert relation.from_id == "test/123" assert relation.to_id == "test/456" @@ -101,18 +109,24 @@ def test_entity_out_from_attributes(): """Test EntityOut creation from database model attributes.""" # Simulate database model attributes db_data = { - "path_id": "test/test", + "permalink": "test/test", "title": "test", "entity_type": "knowledge", "content_type": "text/markdown", "summary": "test description", "observations": [{"id": 1, "content": "test obs", "context": None}], "relations": [ - {"id": 1, "from_id": "test/test", "to_id": "test/test", "relation_type": "test", "context": None} + { + "id": 1, + "from_id": "test/test", + "to_id": "test/test", + "relation_type": "test", + "context": None, + } ], } entity = EntityResponse.model_validate(db_data) - assert entity.path_id == "test/test" + assert entity.permalink == "test/test" assert entity.summary == "test description" assert len(entity.observations) == 1 assert len(entity.relations) == 1 @@ -156,12 +170,12 @@ def test_search_nodes_input(): def test_open_nodes_input(): """Test OpenNodesInput validation.""" - open_input = OpenNodesRequest.model_validate({"path_ids": ["test/test", "test/test2"]}) - assert len(open_input.path_ids) == 2 + open_input = OpenNodesRequest.model_validate({"permalinks": ["test/test", "test/test2"]}) + assert len(open_input.permalinks) == 2 # Empty names list should fail with pytest.raises(ValidationError): - OpenNodesRequest.model_validate({"path_ids": []}) + OpenNodesRequest.model_validate({"permalinks": []}) def test_path_sanitization(): @@ -184,28 +198,15 @@ def test_path_sanitization(): assert result == expected, f"Failed for input: {input_str}" -def test_path_id_generation(): - """Test path_id property generates correct paths.""" +def test_permalink_generation(): + """Test permalink property generates correct paths.""" test_cases = [ - ( - {"title": "BasicMemory", "entity_type": "knowledge"}, - "basic_memory" - ), - ( - {"title": "Memory Service", "entity_type": "knowledge"}, - "memory_service" - ), - ( - {"title": "API Gateway", "entity_type": "knowledge"}, - "api_gateway" - ), - ( - {"title": "TestCase1", "entity_type": "knowledge"}, - "test_case1" - ), + ({"title": "BasicMemory", "entity_type": "knowledge"}, "basic_memory"), + ({"title": "Memory Service", "entity_type": "knowledge"}, "memory_service"), + ({"title": "API Gateway", "entity_type": "knowledge"}, "api_gateway"), + ({"title": "TestCase1", "entity_type": "knowledge"}, "test_case1"), ] for input_data, expected_path in test_cases: entity = Entity.model_validate(input_data) - assert entity.path_id == expected_path, f"Failed for input: {input_data}" - + assert entity.permalink == expected_path, f"Failed for input: {input_data}" diff --git a/tests/services/test_entity_service.py b/tests/services/test_entity_service.py index 6283501c..246d77cf 100644 --- a/tests/services/test_entity_service.py +++ b/tests/services/test_entity_service.py @@ -1,13 +1,11 @@ """Tests for EntityService.""" + from pathlib import Path import pytest -import pytest_asyncio import yaml -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from basic_memory.models import Entity as EntityModel -from basic_memory.repository.entity_repository import EntityRepository from basic_memory.schemas import Entity as EntitySchema from basic_memory.services import FileService from basic_memory.services.entity_service import EntityService @@ -16,7 +14,6 @@ from basic_memory.services.exceptions import EntityNotFoundError pytestmark = pytest.mark.asyncio - async def test_create_entity(entity_service: EntityService, file_service: FileService): """Test successful entity creation.""" entity_data = EntitySchema( @@ -32,7 +29,7 @@ async def test_create_entity(entity_service: EntityService, file_service: FileSe # Assert Entity assert isinstance(entity, EntityModel) assert entity.title == "TestEntity" - assert entity.path_id == entity_data.path_id + assert entity.permalink == entity_data.permalink assert entity.file_path == entity_data.file_path assert entity.entity_type == "test" assert entity.summary == "A test entity description" @@ -40,8 +37,8 @@ async def test_create_entity(entity_service: EntityService, file_service: FileSe assert entity.observations[0].content == "this is a test observation" assert len(entity.relations) == 0 - # Verify we can retrieve it using path_id - retrieved = await entity_service.get_by_path_id(entity_data.path_id) + # Verify we can retrieve it using permalink + retrieved = await entity_service.get_by_permalink(entity_data.permalink) assert retrieved.summary == "A test entity description" assert retrieved.title == "TestEntity" assert retrieved.entity_type == "test" @@ -58,7 +55,7 @@ async def test_create_entity(entity_service: EntityService, file_service: FileSe metadata = yaml.safe_load(frontmatter) # Verify frontmatter contents - assert metadata["id"] == entity.path_id + assert metadata["id"] == entity.permalink assert metadata["type"] == entity.entity_type assert "created" in metadata assert "modified" in metadata @@ -103,11 +100,11 @@ async def test_create_entities(entity_service: EntityService, file_service: File assert entity2.created_at is not None assert entity2.observations[0].content == "this is a test observation" - # Verify we can retrieve them using path_ids - retrieved1 = await entity_service.get_by_path_id(entity_data[0].path_id) + # Verify we can retrieve them using permalinks + retrieved1 = await entity_service.get_by_permalink(entity_data[0].permalink) assert retrieved1.summary == "A test entity description" - retrieved2 = await entity_service.get_by_path_id(entity_data[1].path_id) + retrieved2 = await entity_service.get_by_permalink(entity_data[1].permalink) assert retrieved2.summary == "A test entity description" # verify files are written @@ -116,8 +113,7 @@ async def test_create_entities(entity_service: EntityService, file_service: File assert await file_service.exists(file_path) - -async def test_get_by_path_id(entity_service: EntityService): +async def test_get_by_permalink(entity_service: EntityService): """Test finding entity by type and name combination.""" entity1_data = EntitySchema( title="TestEntity1", @@ -136,14 +132,14 @@ async def test_get_by_path_id(entity_service: EntityService): entity2 = await entity_service.create_entity(entity2_data) # Find by type1 and name - found = await entity_service.get_by_path_id(entity1_data.path_id) + found = await entity_service.get_by_permalink(entity1_data.permalink) assert found is not None assert found.id == entity1.id assert found.entity_type == entity1.entity_type assert found.summary == "First test entity" # Find by type2 and name - found = await entity_service.get_by_path_id(entity2_data.path_id) + found = await entity_service.get_by_permalink(entity2_data.permalink) assert found is not None assert found.id == entity2.id assert found.entity_type == entity2.entity_type @@ -151,7 +147,7 @@ async def test_get_by_path_id(entity_service: EntityService): # Test not found case with pytest.raises(EntityNotFoundError): - await entity_service.get_by_path_id("nonexistent/test_entity") + await entity_service.get_by_permalink("nonexistent/test_entity") async def test_create_entity_no_description(entity_service: EntityService): @@ -162,7 +158,7 @@ async def test_create_entity_no_description(entity_service: EntityService): assert entity.summary is None # Verify after retrieval - retrieved = await entity_service.get_by_path_id(entity_data.path_id) + retrieved = await entity_service.get_by_permalink(entity_data.permalink) assert retrieved.summary is None @@ -177,7 +173,7 @@ async def test_get_entity_success(entity_service: EntityService): await entity_service.create_entity(entity_data) # Get by path ID - retrieved = await entity_service.get_by_path_id(entity_data.path_id) + retrieved = await entity_service.get_by_permalink(entity_data.permalink) assert isinstance(retrieved, EntityModel) assert retrieved.title == "TestEntity" @@ -185,7 +181,6 @@ async def test_get_entity_success(entity_service: EntityService): assert retrieved.summary == "Test description" - async def test_delete_entity_success(entity_service: EntityService): """Test successful entity deletion.""" entity_data = EntitySchema( @@ -195,19 +190,19 @@ async def test_delete_entity_success(entity_service: EntityService): ) await entity_service.create_entity(entity_data) - # Act using path_id - result = await entity_service.delete_entity(entity_data.path_id) + # Act using permalink + result = await entity_service.delete_entity(entity_data.permalink) # Assert assert result is True with pytest.raises(EntityNotFoundError): - await entity_service.get_by_path_id(entity_data.path_id) + await entity_service.get_by_permalink(entity_data.permalink) -async def test_get_entity_by_path_id_not_found(entity_service: EntityService): +async def test_get_entity_by_permalink_not_found(entity_service: EntityService): """Test handling of non-existent entity retrieval.""" with pytest.raises(EntityNotFoundError): - await entity_service.get_by_path_id("test/non_existent") + await entity_service.get_by_permalink("test/non_existent") async def test_delete_nonexistent_entity(entity_service: EntityService): @@ -229,8 +224,8 @@ async def test_create_entity_with_special_chars(entity_service: EntityService): assert entity.title == name assert entity.summary == description - # Verify after retrieval using path_id - retrieved = await entity_service.get_by_path_id(entity_data.path_id) + # Verify after retrieval using permalink + retrieved = await entity_service.get_by_permalink(entity_data.permalink) assert retrieved.summary == description @@ -247,12 +242,12 @@ async def test_create_entity_long_description(entity_service: EntityService): entity = await entity_service.create_entity(entity_data) assert entity.summary == long_description - # Verify after retrieval using path_id - retrieved = await entity_service.get_by_path_id(entity_data.path_id) + # Verify after retrieval using permalink + retrieved = await entity_service.get_by_permalink(entity_data.permalink) assert retrieved.summary == long_description -async def test_open_nodes_by_path_ids(entity_service: EntityService): +async def test_open_nodes_by_permalinks(entity_service: EntityService): """Test opening multiple nodes by path IDs.""" # Create test entities entity1_data = EntitySchema( @@ -271,8 +266,8 @@ async def test_open_nodes_by_path_ids(entity_service: EntityService): await entity_service.create_entity(entity2_data) # Open nodes by path IDs - path_ids = [entity1_data.path_id, entity2_data.path_id] - found = await entity_service.open_nodes(path_ids) + permalinks = [entity1_data.permalink, entity2_data.permalink] + found = await entity_service.open_nodes(permalinks) assert len(found) == 2 names = {e.title for e in found} @@ -297,14 +292,14 @@ async def test_open_nodes_some_not_found(entity_service: EntityService): await entity_service.create_entity(entity_data) # Try to open two nodes, one exists, one doesn't - path_ids = [entity_data.path_id, "type1/non_existent"] - found = await entity_service.open_nodes(path_ids) + permalinks = [entity_data.permalink, "type1/non_existent"] + found = await entity_service.open_nodes(permalinks) assert len(found) == 1 assert found[0].title == "Entity1" -async def test_delete_entities_by_path_ids(entity_service: EntityService): +async def test_delete_entities_by_permalinks(entity_service: EntityService): """Test deleting multiple entities by path IDs.""" # Create test entities entity1_data = EntitySchema( @@ -323,14 +318,14 @@ async def test_delete_entities_by_path_ids(entity_service: EntityService): await entity_service.create_entity(entity2_data) # Delete by path IDs - path_ids = [entity1_data.path_id, entity2_data.path_id] - result = await entity_service.delete_entities(path_ids) + permalinks = [entity1_data.permalink, entity2_data.permalink] + result = await entity_service.delete_entities(permalinks) assert result is True # Verify both are deleted - for path_id in path_ids: + for permalink in permalinks: with pytest.raises(EntityNotFoundError): - await entity_service.get_by_path_id(path_id) + await entity_service.get_by_permalink(permalink) async def test_delete_entities_empty_input(entity_service: EntityService): @@ -341,8 +336,8 @@ async def test_delete_entities_empty_input(entity_service: EntityService): async def test_delete_entities_none_found(entity_service: EntityService): """Test deleting non-existent entities by path IDs.""" - path_ids = ["type1/NonExistent1", "type2/NonExistent2"] - result = await entity_service.delete_entities(path_ids) + permalinks = ["type1/NonExistent1", "type2/NonExistent2"] + result = await entity_service.delete_entities(permalinks) assert result is True @@ -351,7 +346,7 @@ async def test_get_entity_path(entity_service: EntityService): """Should generate correct filesystem path for entity.""" entity = EntityModel( id=1, - path_id="test-entity", + permalink="test-entity", title="test-entity", entity_type="test", summary="Test entity", @@ -360,9 +355,10 @@ async def test_get_entity_path(entity_service: EntityService): assert path == Path(entity_service.file_service.base_path / "test-entity.md") - @pytest.mark.asyncio -async def test_update_knowledge_entity_summary(entity_service: EntityService, file_service: FileService): +async def test_update_knowledge_entity_summary( + entity_service: EntityService, file_service: FileService +): """Should update knowledge entity description and write to file.""" # Create test entity entity = await entity_service.create_entity( @@ -375,9 +371,7 @@ async def test_update_knowledge_entity_summary(entity_service: EntityService, fi ) # Update description - updated = await entity_service.update_entity( - entity.path_id, summary="Updated description" - ) + updated = await entity_service.update_entity(entity.permalink, summary="Updated description") # Verify file has new description but preserved metadata file_path = file_service.get_entity_path(updated) @@ -391,7 +385,6 @@ async def test_update_knowledge_entity_summary(entity_service: EntityService, fi assert metadata["status"] == "draft" - @pytest.mark.asyncio async def test_update_note_entity_content(entity_service: EntityService, file_service: FileService): """Should update note content directly.""" @@ -407,7 +400,7 @@ async def test_update_note_entity_content(entity_service: EntityService, file_se # Update content new_content = "# Updated Content\n\nThis is new content." - updated = await entity_service.update_entity(entity.path_id, content=new_content) + updated = await entity_service.update_entity(entity.permalink, content=new_content) # Verify file has new content but preserved metadata file_path = file_service.get_entity_path(updated) @@ -436,7 +429,7 @@ async def test_update_entity_name(entity_service: EntityService, file_service: F ) # Update name - updated = await entity_service.update_entity(entity.path_id, title="new-name") + updated = await entity_service.update_entity(entity.permalink, title="new-name") # Verify name was updated in DB assert updated.title == "new-name" @@ -447,7 +440,7 @@ async def test_update_entity_name(entity_service: EntityService, file_service: F _, frontmatter, _ = content.split("---", 2) metadata = yaml.safe_load(frontmatter) - assert metadata["id"] == entity.path_id + assert metadata["id"] == entity.permalink # And verify content uses new name for title assert "# new-name" in content diff --git a/tests/services/test_observation_service.py b/tests/services/test_observation_service.py index d22a0839..18cad9df 100644 --- a/tests/services/test_observation_service.py +++ b/tests/services/test_observation_service.py @@ -16,7 +16,7 @@ async def test_add_observations(observation_service: ObservationService, sample_ """Test adding observations to an entity.""" observations = ["Test observation 1", "Test observation 2"] - entity = await observation_service.add_observations(sample_entity.path_id, observations) + entity = await observation_service.add_observations(sample_entity.permalink, observations) observations = entity.observations assert len(observations) == 2 @@ -26,19 +26,23 @@ async def test_add_observations(observation_service: ObservationService, sample_ @pytest.mark.asyncio -async def test_delete_observations(observation_service: ObservationService, entity_service: EntityService, sample_entity: Entity): +async def test_delete_observations( + observation_service: ObservationService, entity_service: EntityService, sample_entity: Entity +): """Test deleting specific observations from an entity.""" # First add observations await observation_service.add_observations( - sample_entity.path_id, ["First observation", "Second observation", "Third observation"] + sample_entity.permalink, ["First observation", "Second observation", "Third observation"] ) # Then delete some contents_to_delete = ["First observation", "Second observation"] - entity = await observation_service.delete_observations(sample_entity.path_id, contents_to_delete) + entity = await observation_service.delete_observations( + sample_entity.permalink, contents_to_delete + ) # Verify - entity = await entity_service.get_by_path_id(sample_entity.path_id) + entity = await entity_service.get_by_permalink(sample_entity.permalink) assert len(entity.observations) == 1 assert entity.observations[0].content == "Third observation" assert entity.observations[0].entity_id == sample_entity.id @@ -49,7 +53,7 @@ async def test_delete_by_entity(observation_service: ObservationService, sample_ """Test deleting all observations for an entity.""" # First add observations await observation_service.add_observations( - sample_entity.path_id, ["First observation", "Second observation"] + sample_entity.permalink, ["First observation", "Second observation"] ) # Delete all observations for entity @@ -65,7 +69,7 @@ async def test_delete_nonexistent_observation( ): """Test deleting observations that don't exist.""" entity = await observation_service.delete_observations( - sample_entity.path_id, ["Nonexistent observation"] + sample_entity.permalink, ["Nonexistent observation"] ) assert entity is not None @@ -73,7 +77,7 @@ async def test_delete_nonexistent_observation( @pytest.mark.asyncio async def test_delete_observations_invalid_entity(observation_service: ObservationService): """Test deleting observations for an entity that doesn't exist.""" - + with pytest.raises(EntityNotFoundError): await observation_service.delete_observations("invalid_entity", ["Test observation"]) @@ -85,7 +89,7 @@ async def test_observation_with_special_characters( """Test handling observations with special characters.""" content = "Test & observation with @#$% special chars!" - entity = await observation_service.add_observations(sample_entity.path_id, [content]) + entity = await observation_service.add_observations(sample_entity.permalink, [content]) assert len(entity.observations) == 1 assert entity.observations[0].content == content @@ -97,7 +101,7 @@ async def test_very_long_observation( """Test handling very long observation content.""" long_content = "Very long observation " * 100 # ~1800 characters - entity = await observation_service.add_observations(sample_entity.path_id, [long_content]) + entity = await observation_service.add_observations(sample_entity.permalink, [long_content]) assert len(entity.observations) == 1 assert entity.observations[0].content == long_content @@ -137,14 +141,13 @@ async def test_add_observations_with_categories( ObservationCreate(content="Design observation", category=ObservationCategory.DESIGN), ] - entity = await observation_service.add_observations(sample_entity.path_id, observations) + entity = await observation_service.add_observations(sample_entity.permalink, observations) assert len(entity.observations) == 2 assert entity.observations[0].category == ObservationCategory.TECH.value assert entity.observations[1].category == ObservationCategory.DESIGN.value - @pytest.mark.asyncio async def test_get_observations_by_category( observation_service: ObservationService, sample_entity: Entity @@ -156,7 +159,7 @@ async def test_get_observations_by_category( ObservationCreate(content="Design decision", category=ObservationCategory.DESIGN), ObservationCreate(content="Second tech note", category=ObservationCategory.TECH), ] - await observation_service.add_observations(sample_entity.path_id, observations) + await observation_service.add_observations(sample_entity.permalink, observations) # Get tech observations tech_obs = await observation_service.get_observations_by_category(ObservationCategory.TECH) @@ -182,7 +185,7 @@ async def test_observation_categories( ObservationCreate(content="Another tech note", category=ObservationCategory.TECH), ObservationCreate(content="Feature note", category=ObservationCategory.FEATURE), ] - await observation_service.add_observations(sample_entity.path_id, observations) + await observation_service.add_observations(sample_entity.permalink, observations) # Get categories categories = await observation_service.observation_categories() @@ -202,7 +205,7 @@ async def test_default_category_behavior( observations = [ ObservationCreate(content="Simple note") # No category specified ] - entity = await observation_service.add_observations(sample_entity.path_id, observations) + entity = await observation_service.add_observations(sample_entity.permalink, observations) assert len(entity.observations) == 1 assert entity.observations[0].category == ObservationCategory.NOTE.value # Should use default diff --git a/tests/services/test_relation_service.py b/tests/services/test_relation_service.py index 599fa634..fbf6288f 100644 --- a/tests/services/test_relation_service.py +++ b/tests/services/test_relation_service.py @@ -19,7 +19,7 @@ async def test_entities( entity1 = EntityModel( title="test_entity_1", entity_type="test", - path_id="test/test_entity_1", + permalink="test/test_entity_1", file_path="test/test_entity_1.md", summary="Test entity 1", content_type="text/markdown", @@ -27,7 +27,7 @@ async def test_entities( entity2 = EntityModel( title="test_entity_2", entity_type="test", - path_id="test/test_entity_2", + permalink="test/test_entity_2", file_path="test/test_entity_2.md", summary="Test entity 2", content_type="text/markdown", @@ -49,10 +49,16 @@ async def test_create_relations( relation_data = [ RelationSchema( - from_id=entity1.path_id, to_id=entity2.path_id, relation_type="type_0", context="context_0" + from_id=entity1.permalink, + to_id=entity2.permalink, + relation_type="type_0", + context="context_0", ), RelationSchema( - from_id=entity1.path_id, to_id=entity2.path_id, relation_type="type_1", context="context_1" + from_id=entity1.permalink, + to_id=entity2.permalink, + relation_type="type_1", + context="context_1", ), ] @@ -85,16 +91,16 @@ async def test_create_relations( assert relations_e1[1].relation_type == "type_1" # Verify outgoing relation is updated - found = await entity_service.get_by_path_id(entity1.path_id) + found = await entity_service.get_by_permalink(entity1.permalink) file_path = file_service.get_entity_path(found) content, _ = await file_service.read_file(file_path) - + # verify relation format assert f"- type_0 [[{entity2.title}]] (context_0)" in content assert f"- type_1 [[{entity2.title}]] (context_1)" in content # Verify other entity file is not updated - found = await entity_service.get_by_path_id(entity2.path_id) + found = await entity_service.get_by_permalink(entity2.permalink) file_path = file_service.get_entity_path(found) content, _ = await file_service.read_file(file_path) assert "type_0" not in content @@ -109,7 +115,7 @@ async def test_delete_relation( # Create a relation first relation_data = RelationSchema( - from_id=entity1.path_id, to_id=entity2.path_id, relation_type="test_relation" + from_id=entity1.permalink, to_id=entity2.permalink, relation_type="test_relation" ) await relation_service.create_relations([relation_data]) @@ -140,10 +146,10 @@ async def test_delete_relations_by_criteria( # Create test relations relation1 = RelationSchema( - from_id=entity1.path_id, to_id=entity2.path_id, relation_type="relation1" + from_id=entity1.permalink, to_id=entity2.permalink, relation_type="relation1" ) relation2 = RelationSchema( - from_id=entity1.path_id, to_id=entity2.path_id, relation_type="relation2" + from_id=entity1.permalink, to_id=entity2.permalink, relation_type="relation2" ) await relation_service.create_relations([relation1, relation2]) diff --git a/tests/services/test_search_service.py b/tests/services/test_search_service.py index bee527de..13a6808b 100644 --- a/tests/services/test_search_service.py +++ b/tests/services/test_search_service.py @@ -17,7 +17,7 @@ def test_entity(): title = "TestComponent" entity_type = "knowledge" entity_metadata = {"test": "test"} - path_id = "component/test_component" + permalink = "component/test_component" file_path = "entities/component/test_component.md" summary = "A test component for search" content_type = "text/markdown" @@ -35,7 +35,7 @@ def test_document(): class Document: id = 1 - path_id = "docs/test_doc.md" + permalink = "docs/test_doc.md" file_path = "docs/test_doc.md" doc_metadata = {"title": "Test Document", "type": "technical"} created_at = datetime.now(timezone.utc) @@ -63,7 +63,7 @@ async def test_index_entity(search_service, test_entity): # Search for the entity results = await search_service.search(SearchQuery(text="test component")) assert len(results) == 1 - assert results[0].path_id == test_entity.path_id + assert results[0].permalink == test_entity.permalink assert results[0].type == SearchItemType.ENTITY @@ -138,7 +138,7 @@ async def test_reindex_all(search_service, entity_service, session_maker): SearchQuery(text="TestComponent", types=[SearchItemType.ENTITY]) ) assert len(entity_results) == 1 - assert entity_results[0].path_id == test_entity.path_id + assert entity_results[0].permalink == test_entity.permalink assert entity_results[0].type == SearchItemType.ENTITY diff --git a/tests/sync/test_file_change_scanner.py b/tests/sync/test_file_change_scanner.py index 520e68ff..31e7a675 100644 --- a/tests/sync/test_file_change_scanner.py +++ b/tests/sync/test_file_change_scanner.py @@ -3,7 +3,6 @@ from pathlib import Path import pytest -from multipart import file_path from basic_memory.models import Entity from basic_memory.sync import FileChangeScanner @@ -67,7 +66,8 @@ async def test_scan_with_unreadable_file(file_change_scanner: FileChangeScanner, @pytest.mark.asyncio async def test_detect_new_files( - file_change_scanner: FileChangeScanner, temp_dir: Path, + file_change_scanner: FileChangeScanner, + temp_dir: Path, ): """Test detection of new files.""" # Create new file @@ -91,7 +91,9 @@ async def test_detect_modified_file(file_change_scanner: FileChangeScanner, temp # Create DB state with original checksum original_checksum = await compute_checksum(content) - db_records = {file_path: FileState(file_path=file_path, path_id="test", checksum=original_checksum)} + db_records = { + file_path: FileState(file_path=file_path, permalink="test", checksum=original_checksum) + } # Modify file await create_test_file(temp_dir / file_path, "modified") @@ -108,7 +110,9 @@ async def test_detect_deleted_files(file_change_scanner: FileChangeScanner, temp file_path = "deleted.md" # Create DB state with file that doesn't exist - db_records = {file_path: FileState(file_path=file_path, path_id="deleted", checksum="any-checksum")} + db_records = { + file_path: FileState(file_path=file_path, permalink="deleted", checksum="any-checksum") + } changes = await file_change_scanner.find_changes(directory=temp_dir, db_file_state=db_records) @@ -116,11 +120,10 @@ async def test_detect_deleted_files(file_change_scanner: FileChangeScanner, temp assert file_path in changes.deleted - @pytest.mark.asyncio async def test_get_db_state_entities(file_change_scanner: FileChangeScanner): """Test converting entity records to file states.""" - entity = Entity(path_id="concept/test", file_path="concept/test.md", checksum="test-checksum") + entity = Entity(permalink="concept/test", file_path="concept/test.md", checksum="test-checksum") db_records = await file_change_scanner.get_db_file_state([entity]) @@ -129,7 +132,6 @@ async def test_get_db_state_entities(file_change_scanner: FileChangeScanner): assert db_records["concept/test.md"].checksum == "test-checksum" - @pytest.mark.asyncio async def test_empty_directory(file_change_scanner: FileChangeScanner, temp_dir: Path): """Test handling empty/nonexistent directory.""" diff --git a/tests/sync/test_knowledge_sync_service.py b/tests/sync/test_knowledge_sync_service.py index 4b4f7cce..e8a27c07 100644 --- a/tests/sync/test_knowledge_sync_service.py +++ b/tests/sync/test_knowledge_sync_service.py @@ -48,9 +48,7 @@ def test_content() -> EntityContent: @pytest_asyncio.fixture def test_markdown(test_frontmatter, test_content) -> EntityMarkdown: """Create complete test markdown entity.""" - return EntityMarkdown( - frontmatter=test_frontmatter, content=test_content - ) + return EntityMarkdown(frontmatter=test_frontmatter, content=test_content) @pytest.mark.asyncio @@ -64,7 +62,7 @@ async def test_create_entity_without_relations( # Check basic fields assert entity.title == "Test Entity" assert entity.entity_type == "knowledge" - assert entity.path_id == "concept/test_entity" + assert entity.permalink == "concept/test_entity" assert entity.summary == "A test entity description" # Check observations @@ -94,7 +92,7 @@ async def test_update_entity_without_relations( # Update entity updated = await knowledge_sync_service.update_entity_and_observations( - entity.path_id, test_markdown + entity.permalink, test_markdown ) # Check fields updated @@ -119,14 +117,14 @@ async def test_update_entity_relations( other_entity = EntityModel( title="Other Entity", entity_type="test", - path_id="concept/other_entity", + permalink="concept/other_entity", file_path="concept/other_entity.md", content_type="text/markdown", ) another_entity = EntityModel( title="Another Entity", entity_type="test", - path_id="concept/another_entity", + permalink="concept/another_entity", file_path="concept/another_entity.md", content_type="text/markdown", ) @@ -164,14 +162,14 @@ async def test_two_pass_sync_flow( other_entity = EntityModel( title="Other Entity", entity_type="test", - path_id="concept/other_entity", + permalink="concept/other_entity", file_path="concept/other_entity.md", content_type="text/markdown", ) another_entity = EntityModel( title="Another Entity", entity_type="test", - path_id="concept/another_entity", + permalink="concept/another_entity", file_path="concept/another_entity.md", content_type="text/markdown", ) diff --git a/tests/sync/test_sync_knowledge.py b/tests/sync/test_sync_knowledge.py index b08a7deb..93bbfc34 100644 --- a/tests/sync/test_sync_knowledge.py +++ b/tests/sync/test_sync_knowledge.py @@ -46,7 +46,7 @@ A test concept. # Create related entity in DB that will be deleted other = Entity( - path_id="concept/other", + permalink="concept/other", title="Other", entity_type="test", file_path="concept/other.md", @@ -63,12 +63,12 @@ A test concept. assert len(entities) == 1 # Find new entity - test_concept = next(e for e in entities if e.path_id == "concept/test_concept") + test_concept = next(e for e in entities if e.permalink == "concept/test_concept") assert test_concept.entity_type == "knowledge" # Verify relation was not created # because file for related entity was not found - entity = await entity_service.get_by_path_id(test_concept.path_id) + entity = await entity_service.get_by_permalink(test_concept.permalink) relations = entity.relations assert len(relations) == 0 @@ -103,7 +103,7 @@ modified: 2024-01-01 await sync_service.sync(test_config.home) # Verify entity created but no relations - entity = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/depends_on_future" ) assert entity is not None @@ -157,10 +157,10 @@ modified: 2024-01-01 await sync_service.sync(test_config.home) # Verify both entities and their relations - entity_a = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity_a = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/entity_a" ) - entity_b = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity_b = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/entity_b" ) @@ -232,7 +232,7 @@ modified: 2024-01-01 await sync_service.sync(test_config.home) # Verify duplicates are handled - entity = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/duplicate_relations" ) @@ -274,7 +274,7 @@ modified: 2024-01-01 await sync_service.sync(test_config.home) # Verify observations - entity = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/invalid_category" ) @@ -353,13 +353,13 @@ modified: 2024-01-01 await sync_service.sync(test_config.home) # Verify all relations are created correctly regardless of order - entity_a = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity_a = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/entity_a" ) - entity_b = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity_b = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/entity_b" ) - entity_c = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id( + entity_c = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink( "concept/entity_c" ) diff --git a/tests/sync/test_sync_service.py b/tests/sync/test_sync_service.py index 91e507d1..beafe2a7 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.home), modify_file()) # Verify final state - doc = await sync_service.knowledge_sync_service.entity_repository.get_by_path_id("changing") + doc = await sync_service.knowledge_sync_service.entity_repository.get_by_permalink("changing") assert doc is not None # File should have a checksum, even if it's from either version assert doc.checksum is not None @@ -71,7 +71,7 @@ async def test_sync_null_checksum_cleanup( """Test handling of entities with null checksums from incomplete syncs.""" # Create entity with null checksum (simulating incomplete sync) entity = Entity( - path_id="concept/incomplete", + permalink="concept/incomplete", title="Incomplete", entity_type="test", file_path="concept/incomplete.md", @@ -99,5 +99,5 @@ modified: 2024-01-01 await sync_service.sync(test_config.home) # Verify entity was properly synced - updated = await entity_service.get_by_path_id("concept/incomplete") + updated = await entity_service.get_by_permalink("concept/incomplete") assert updated.checksum is not None