change entity.path_id to entity.permalink

This commit is contained in:
phernandez
2025-01-12 16:47:21 -06:00
parent 1f374e0133
commit c5d21276de
52 changed files with 714 additions and 687 deletions
+43 -34
View File
@@ -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]]
return [path for path, _ in sorted_paths[:limit]]
+22 -22
View File
@@ -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)
+4 -9
View File
@@ -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
@@ -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}")
+14 -12
View File
@@ -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
+7 -7
View File
@@ -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)