add get_recent_activity endpoint

This commit is contained in:
phernandez
2024-12-30 20:32:55 -06:00
parent 7a06b87bd1
commit 1507875785
10 changed files with 445 additions and 99 deletions
+2
View File
@@ -9,6 +9,7 @@ from basic_memory import db
from .routers import documents
from .routers import knowledge
from .routers import discovery
from .routers.activity_router import router as activity_router
@asynccontextmanager
@@ -32,3 +33,4 @@ app = FastAPI(
app.include_router(knowledge.router)
app.include_router(documents.router)
app.include_router(discovery.router)
app.include_router(activity_router)
@@ -0,0 +1,50 @@
"""Activity router for tracking recent changes."""
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from loguru import logger
from basic_memory.deps import get_activity_service
from basic_memory.services.activity_service import ActivityService
from basic_memory.schemas.activity import RecentActivity, ActivityType
router = APIRouter(
prefix="/activity",
tags=["activity"]
)
@router.get(
"/recent",
response_model=RecentActivity,
summary="Get recent activity"
)
async def get_recent_activity(
activity_service: ActivityService = Depends(get_activity_service),
timeframe: str = "1d",
activity_types: Optional[List[str]] = Query(None), # Use Query for array params
include_content: bool = True
) -> RecentActivity:
"""
Get recent activity across the knowledge base.
Args:
timeframe: Time window to look back (1h, 1d, 1w, 1m)
activity_types: Optional list of types to include
include_content: Whether to include full content
Returns:
RecentActivity with changes and summary
"""
logger.debug(
f"Getting recent activity (timeframe={timeframe}, "
f"types={activity_types}, include_content={include_content})"
)
return await activity_service.get_recent_activity(
timeframe=timeframe,
activity_types=activity_types,
include_content=include_content
)
+17
View File
@@ -23,6 +23,7 @@ from basic_memory.services import (
RelationService,
DocumentService,
)
from basic_memory.services.activity_service import ActivityService
from basic_memory.services.file_service import FileService
from basic_memory.services.knowledge import KnowledgeService
@@ -143,6 +144,22 @@ async def get_document_service(
DocumentServiceDep = Annotated[DocumentService, Depends(get_document_service)]
async def get_activity_service(
entity_service: EntityServiceDep,
document_service: DocumentServiceDep,
relation_service: RelationServiceDep,
) -> ActivityService:
"""Create ActivityService with dependencies."""
return ActivityService(
entity_service=entity_service,
document_service=document_service,
relation_service=relation_service
)
ActivityServiceDep = Annotated[ActivityService, Depends(get_activity_service)]
async def get_file_service() -> FileService:
return FileService()
+9
View File
@@ -10,6 +10,7 @@ from basic_memory.mcp.tools import knowledge # noqa: F401
from basic_memory.mcp.tools import search # noqa: F401
from basic_memory.mcp.tools import documents # noqa: F401
from basic_memory.mcp.tools import discovery # noqa: F401
from basic_memory.mcp.tools import activity # noqa: F401
# Export the tools
from basic_memory.mcp.tools.knowledge import (
@@ -40,6 +41,11 @@ from basic_memory.mcp.tools.discovery import (
get_observation_categories,
)
from basic_memory.mcp.tools.activity import (
get_recent_activity,
)
__all__ = [
# Knowledge graph tools
"create_entities",
@@ -64,4 +70,7 @@ __all__ = [
# Discovery tools
"get_entity_types",
"get_observation_categories",
# Activity tools
"get_recent_activity",
]
+60
View File
@@ -0,0 +1,60 @@
"""Tools for tracking activity and changes in the knowledge base."""
from typing import List, Optional
from loguru import logger
from basic_memory.mcp.async_client import client
from basic_memory.mcp.server import mcp
from basic_memory.schemas.activity import ActivityType, RecentActivity
@mcp.tool()
async def get_recent_activity(
timeframe: str = "1d",
activity_types: Optional[List[ActivityType]] = None,
include_content: bool = True
) -> RecentActivity:
"""
Get recent activity across your knowledge base.
Shows you what has changed recently including:
- Document changes
- Entity updates
- Relation modifications
You can filter by:
- Timeframe (e.g., 1h, 1d, 1w, 1m)
- Activity types (document, entity, relation)
- Whether to include content
Examples:
# Get all activity in last day
activity = await get_recent_activity()
# Get only document changes
docs = await get_recent_activity(
timeframe="1h",
activity_types=[ActivityType.DOCUMENT],
include_content=False
)
Returns:
RecentActivity object with changes and summary
"""
logger.debug(
f"Getting recent activity (timeframe={timeframe}, "
f"types={activity_types}, include_content={include_content})"
)
# Build params
params = {
"timeframe": timeframe,
"include_content": str(include_content).lower()
}
if activity_types:
params["activity_types"] = [t.value for t in activity_types] # Convert enums to values
# Get activity
response = await client.get("/activity/recent", params=params)
return RecentActivity.model_validate(response.json())
+18
View File
@@ -0,0 +1,18 @@
"""Document related schemas."""
from typing import Dict, Optional
from pydantic import BaseModel, Field
from basic_memory.schemas.request import DocumentPathId
class CreateDocumentRequest(BaseModel):
"""Request to create a new document."""
path_id: DocumentPathId = Field(..., description="Path to the document")
content: str = Field(..., description="Document content")
doc_metadata: Optional[Dict] = Field(default=None, description="Optional metadata")
class DocumentUpdate(BaseModel):
"""Request to update a document."""
content: Optional[str] = None
doc_metadata: Optional[Dict] = None
+28 -23
View File
@@ -1,4 +1,6 @@
from datetime import datetime
"""Service for tracking and querying activity across the knowledge base."""
from datetime import datetime, timezone
from typing import List, Optional, Sequence
from . import EntityService, DocumentService, RelationService
@@ -45,7 +47,7 @@ class ActivityService:
"""
# Parse timeframe and get cutoff date
tf = TimeFrame(timeframe)
since = datetime.utcnow() - tf.to_timedelta
since = datetime.now(timezone.utc) - tf.to_timedelta
# Get changes based on requested types
changes = []
@@ -63,7 +65,10 @@ class ActivityService:
elif activity_type == ActivityType.RELATION:
changes.extend(await self._get_relation_changes(since))
# Sort all changes by timestamp
# Sort all changes by timestamp, ensuring timezone awareness
for change in changes:
if change.timestamp.tzinfo is None:
change.timestamp = change.timestamp.replace(tzinfo=timezone.utc)
changes.sort(key=lambda x: x.timestamp, reverse=True)
# Remove content if not requested
@@ -92,17 +97,17 @@ class ActivityService:
changes = []
for entity in entities:
change_type = (
ChangeType.CREATED
if entity.created_at >= since
else ChangeType.UPDATED
)
# 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
change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED
changes.append(
ActivityChange(
activity_type=ActivityType.ENTITY,
change_type=change_type,
timestamp=entity.updated_at,
timestamp=updated_at,
path_id=entity.path_id,
summary=f"{change_type.value.title()} entity: {entity.name}",
content=entity.description
@@ -118,20 +123,20 @@ class ActivityService:
changes = []
for doc in documents:
change_type = (
ChangeType.CREATED
if doc.created_at >= since
else ChangeType.UPDATED
)
# Ensure timestamps are timezone-aware
created_at = doc.created_at.replace(tzinfo=timezone.utc) if doc.created_at.tzinfo is None else doc.created_at
updated_at = doc.updated_at.replace(tzinfo=timezone.utc) if doc.updated_at.tzinfo is None else doc.updated_at
change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED
changes.append(
ActivityChange(
activity_type=ActivityType.DOCUMENT,
change_type=change_type,
timestamp=doc.updated_at,
timestamp=updated_at,
path_id=doc.path_id,
summary=f"{change_type.value.title()} document: {doc.path_id}",
#content=doc.content[:500] if doc.content else None # First 500 chars
content=None # For documents we don't include content by default
)
)
@@ -144,17 +149,17 @@ class ActivityService:
changes = []
for relation in relations:
change_type = (
ChangeType.CREATED
if relation.created_at >= since
else ChangeType.UPDATED
)
# 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
change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED
changes.append(
ActivityChange(
activity_type=ActivityType.RELATION,
change_type=change_type,
timestamp=relation.updated_at,
timestamp=updated_at,
path_id=f"{relation.from_id}->{relation.to_id}",
summary=(
f"{change_type.value.title()} relation: "
@@ -178,4 +183,4 @@ class ActivityService:
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]]