fix all tests for observation category

This commit is contained in:
phernandez
2024-12-25 14:16:46 -06:00
parent fd723164ca
commit bb47fbd48b
12 changed files with 536 additions and 136 deletions
+32 -21
View File
@@ -1,6 +1,5 @@
"""Writer for knowledge entity markdown files."""
from datetime import datetime, UTC
from typing import Optional, Dict, Any
import yaml
@@ -14,12 +13,11 @@ class KnowledgeWriter:
async def format_frontmatter(self, entity: EntityModel) -> dict:
"""Generate frontmatter metadata for entity."""
now = datetime.now(UTC).isoformat()
return {
"type": entity.entity_type,
"id": entity.id,
"created": now,
"modified": now
"id": entity.path_id,
"created": entity.created_at.isoformat(),
"modified": entity.updated_at.isoformat(),
}
async def format_metadata(self, metadata: Optional[Dict[str, Any]] = None) -> str:
@@ -40,37 +38,50 @@ class KnowledgeWriter:
logger.warning(f"Failed to format metadata YAML: {e}")
return "" # Skip metadata on error
async def format_content(self, entity: EntityModel, metadata: Optional[Dict[str, Any]] = None) -> str:
async def format_content(
self, entity: EntityModel, metadata: Optional[Dict[str, Any]] = None
) -> str:
"""Format entity content as markdown."""
sections = [
f"# {entity.name}\n"
f"# {entity.name}\n",
"", # Empty line after name
]
if entity.description:
sections.extend([
entity.description,
""
])
sections.extend([entity.description, ""])
if entity.observations:
sections.extend([
"## Observations",
*[f"- {obs.content}" for obs in entity.observations],
""
])
sections.extend(
[
"## Observations",
"<!-- Format: - [category] Content text #tag1 #tag2 (optional context) -->",
"", # Empty line after format comment
*[
f"- [{obs.category}] {obs.content}"
+ (f" ({obs.context})" if obs.context else "")
for obs in entity.observations
],
"",
]
)
# Format outgoing and incoming relations separately
if entity.to_relations or entity.from_relations:
sections.append("## Relations")
sections.extend(
[
"## Relations",
"", # Empty line after format comment
]
)
# Outgoing relations
for rel in entity.to_relations:
sections.append(f"- [[{rel.from_entity.name}]] {rel.relation_type}")
# Incoming relations
for rel in entity.from_relations:
sections.append(f"- [[{rel.to_entity.name}]] {rel.relation_type}")
sections.append("")
if metadata:
+2 -1
View File
@@ -2,12 +2,13 @@
from basic_memory.models.base import Base
from basic_memory.models.documents import Document
from basic_memory.models.knowledge import Entity, Observation, Relation
from basic_memory.models.knowledge import Entity, Observation, Relation, ObservationCategory
__all__ = [
'Base',
'Document',
'Entity',
'Observation',
'ObservationCategory',
'Relation'
]
@@ -26,3 +26,15 @@ class ObservationRepository(Repository[Observation]):
query = select(Observation).filter(Observation.context == context)
result = await self.execute_query(query)
return result.scalars().all()
async def find_by_category(self, category: str) -> Sequence[Observation]:
"""Find observations with a specific context."""
query = select(Observation).filter(Observation.category == category)
result = await self.execute_query(query)
return result.scalars().all()
async def observation_categories(self) -> Sequence[str]:
"""Return a list of all observation categories."""
query = select(Observation.category).distinct()
result = await self.execute_query(query)
return result.scalars().all()
+2
View File
@@ -127,6 +127,7 @@ class SearchNodesRequest(BaseModel):
- Partial word matches
- Returns full entity objects with relations
- Includes all matching entities
- If a category is specified, only entities with that category are returned
Example Queries:
- "memory" - Find entities related to memory systems
@@ -143,6 +144,7 @@ class SearchNodesRequest(BaseModel):
"""
query: Annotated[str, MinLen(1), MaxLen(200)]
category: Optional[ObservationCategory] = None
class OpenNodesRequest(BaseModel):
@@ -7,6 +7,7 @@ from loguru import logger
from basic_memory.models import Entity as EntityModel
from basic_memory.services.exceptions import EntityNotFoundError
from basic_memory.services.observation_service import ObservationService
from basic_memory.schemas.request import ObservationCreate
from .relations import RelationOperations
@@ -18,9 +19,22 @@ class ObservationOperations(RelationOperations):
self.observation_service = observation_service
async def add_observations(
self, path_id: str, observations: List[str], context: str | None = None
self,
path_id: str,
observations: List[ObservationCreate],
context: str | None = None
) -> EntityModel:
"""Add observations to entity and update its file."""
"""Add observations to entity and update its file.
Observations are added with their categories and written to both
the database and markdown file. The file format is:
- [category] Content text #tag1 #tag2 (optional context)
Args:
path_id: 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}")
try:
@@ -33,25 +47,34 @@ class ObservationOperations(RelationOperations):
await self.observation_service.add_observations(entity.id, observations, context)
# Get updated entity
updated_entity = await self.entity_service.get_by_path_id(path_id)
entity = await self.entity_service.get_by_path_id(path_id)
# Write updated file and checksum
_, checksum = await self.write_entity_file(entity)
await self.entity_service.update_entity(path_id, {"checksum": checksum})
# query to fetch all relations
# Return final entity with all updates and relations
return await self.entity_service.get_by_path_id(path_id)
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:
"""Delete observations from entity and update its file."""
async def delete_observations(
self,
path_id: str,
observations: List[str]
) -> EntityModel:
"""Delete observations from entity and update its file.
Args:
path_id: Entity path ID
observations: List of observation contents to delete
"""
logger.debug(f"Deleting observations from entity {path_id}")
try:
# Get updated entity
# Get entity
entity = await self.entity_service.get_by_path_id(path_id)
if not entity:
raise EntityNotFoundError(f"Entity not found: {path_id}")
@@ -63,9 +86,9 @@ class ObservationOperations(RelationOperations):
_, checksum = await self.write_entity_file(entity)
await self.entity_service.update_entity(path_id, {"checksum": checksum})
# Get final entity with all updates
# Return final entity with all updates
return await self.entity_service.get_by_path_id(path_id)
except Exception as e:
logger.error(f"Failed to delete observations: {e}")
raise
raise
@@ -1,6 +1,6 @@
"""Service for managing observations in the database."""
from typing import List, Sequence
from typing import List, Sequence, Optional
from loguru import logger
from sqlalchemy import select
@@ -8,6 +8,8 @@ from sqlalchemy import select
from basic_memory.models import Observation as ObservationModel
from basic_memory.repository.observation_repository import ObservationRepository
from .service import BaseService
from ..schemas.base import ObservationCategory
from ..schemas.request import ObservationCreate
class ObservationService(BaseService[ObservationRepository]):
@@ -20,13 +22,22 @@ class ObservationService(BaseService[ObservationRepository]):
super().__init__(observation_repository)
async def add_observations(
self, entity_id: int, observations: List[str], context: str | None = None
self,
entity_id: int,
observations: List[str | ObservationCreate],
context: Optional[str] = None,
) -> Sequence[ObservationModel]:
"""Add multiple observations to an entity."""
logger.debug(f"Adding {len(observations)} observations to entity: {entity_id}")
return await self.repository.create_all(
[
dict(entity_id=entity_id, content=observation, context=context)
# unpack the ObservationCreate values if present
dict(
entity_id=entity_id,
content=getattr(observation, "content", observation),
context=context,
category=getattr(observation, "category", None),
)
for observation in observations
]
)
@@ -46,14 +57,20 @@ class ObservationService(BaseService[ObservationRepository]):
logger.debug(f"Deleting all observations for entity: {entity_id}")
return await self.repository.delete_by_fields(entity_id=entity_id)
async def search_observations(self, query: str) -> List[ObservationModel]:
async def search_observations(self, query: str, category: Optional[ObservationCategory] = None) -> List[ObservationModel]:
"""Search for observations across all entities."""
logger.debug(f"Searching observations with query: {query}")
result = await self.repository.execute_query(
select(ObservationModel).filter(
ObservationModel.content.contains(query) | ObservationModel.context.contains(query)
)
# Build base query
statement = select(ObservationModel).filter(
ObservationModel.content.contains(query) | ObservationModel.context.contains(query)
)
# Add category filter if specified
if category:
statement = statement.filter(ObservationModel.category == category)
result = await self.repository.execute_query(statement)
observations = result.scalars().all()
return [ObservationModel(content=obs.content) for obs in observations]
@@ -61,3 +78,13 @@ class ObservationService(BaseService[ObservationRepository]):
"""Get all observations with a specific context."""
logger.debug(f"Getting observations for context: {context}")
return await self.repository.find_by_context(context)
async def get_observations_by_category(self, category: ObservationCategory) -> Sequence[ObservationModel]:
"""Get all observations with a specific context."""
logger.debug(f"Getting observations for context: {category}")
return await self.repository.find_by_category(category)
async def observation_categories(self) -> Sequence[str]:
"""Get all observation categories."""
logger.debug("Getting observations categories")
return await self.repository.observation_categories()