mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
add get_recent_activity endpoint
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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]]
|
||||
Reference in New Issue
Block a user