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]]
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Test activity router."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from basic_memory.schemas.activity import ActivityType
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_recent_activity(client: AsyncClient):
|
||||
"""Test getting recent activity."""
|
||||
# Get initial activity
|
||||
response = await client.get("/activity/recent")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Parse response
|
||||
data = response.json()
|
||||
assert "changes" in data
|
||||
assert "summary" in data
|
||||
assert "timeframe" in data
|
||||
assert data["timeframe"] == "1d" # Default timeframe
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_recent_activity_with_filters(client: AsyncClient):
|
||||
"""Test getting recent activity with filters."""
|
||||
# Get activity with filters
|
||||
response = await client.get(
|
||||
"/activity/recent",
|
||||
params={
|
||||
"timeframe": "1h",
|
||||
"activity_types": [ActivityType.DOCUMENT.value],
|
||||
"include_content": False
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Parse response
|
||||
data = response.json()
|
||||
assert data["timeframe"] == "1h"
|
||||
|
||||
# Verify all changes are document type
|
||||
for change in data["changes"]:
|
||||
assert change["activity_type"] == ActivityType.DOCUMENT.value
|
||||
assert change["content"] is None # Content excluded
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Tests for activity tracking MCP tool."""
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.mcp.tools.documents import create_document
|
||||
from basic_memory.mcp.tools.knowledge import create_entities
|
||||
from basic_memory.mcp.tools.activity import get_recent_activity
|
||||
from basic_memory.schemas.base import Entity
|
||||
from basic_memory.schemas.request import CreateEntityRequest, DocumentRequest
|
||||
from basic_memory.schemas.activity import ActivityType
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_basic(client):
|
||||
"""Test getting activity after creating some documents."""
|
||||
# Create test documents
|
||||
doc1 = await create_document(
|
||||
DocumentRequest(
|
||||
path_id="test/doc1.md",
|
||||
content="Test document 1",
|
||||
doc_metadata={}
|
||||
)
|
||||
)
|
||||
doc2 = await create_document(
|
||||
DocumentRequest(
|
||||
path_id="test/doc2.md",
|
||||
content="Test document 2",
|
||||
doc_metadata={}
|
||||
)
|
||||
)
|
||||
|
||||
# Get recent activity
|
||||
result = await get_recent_activity()
|
||||
|
||||
# Should find both documents
|
||||
assert len(result.changes) == 2
|
||||
assert result.summary.document_changes == 2
|
||||
assert result.summary.entity_changes == 0
|
||||
assert result.summary.relation_changes == 0
|
||||
|
||||
# Should be sorted by timestamp
|
||||
timestamps = [change.timestamp for change in result.changes]
|
||||
assert timestamps == sorted(timestamps, reverse=True)
|
||||
|
||||
# Should have both documents
|
||||
paths = {change.path_id for change in result.changes}
|
||||
assert "test/doc1.md" in paths
|
||||
assert "test/doc2.md" in paths
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_filtered(client):
|
||||
"""Test activity filtering by type."""
|
||||
# Create both document and entity changes
|
||||
doc = await create_document(
|
||||
DocumentRequest(
|
||||
path_id="test/filtered_doc.md",
|
||||
content="Test document",
|
||||
doc_metadata={}
|
||||
)
|
||||
)
|
||||
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[Entity(name="TestEntity", entity_type="test")]
|
||||
)
|
||||
entity = await create_entities(entity_request)
|
||||
|
||||
# Get only document changes
|
||||
result = await get_recent_activity(
|
||||
timeframe="1d",
|
||||
activity_types=[ActivityType.DOCUMENT] # Use enum directly
|
||||
)
|
||||
|
||||
# Should only find document changes
|
||||
assert len(result.changes) == 1
|
||||
assert result.summary.document_changes == 1
|
||||
assert result.summary.entity_changes == 0
|
||||
assert result.changes[0].activity_type == ActivityType.DOCUMENT
|
||||
assert result.changes[0].path_id == "test/filtered_doc.md"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_without_content(client):
|
||||
"""Test getting activity without content."""
|
||||
# Create test document
|
||||
doc = await create_document(
|
||||
DocumentRequest(
|
||||
path_id="test/no_content_doc.md",
|
||||
content="This content should not appear",
|
||||
doc_metadata={}
|
||||
)
|
||||
)
|
||||
|
||||
# Get activity without content
|
||||
result = await get_recent_activity(
|
||||
timeframe="1d",
|
||||
include_content=False
|
||||
)
|
||||
|
||||
# Should find doc but without content
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].content is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_multiple_types(client):
|
||||
"""Test tracking activity across all types."""
|
||||
# Create changes of different types
|
||||
doc = await create_document(
|
||||
DocumentRequest(
|
||||
path_id="test/activity_doc.md",
|
||||
content="Test document",
|
||||
doc_metadata={}
|
||||
)
|
||||
)
|
||||
|
||||
entity_request = CreateEntityRequest(
|
||||
entities=[
|
||||
Entity(name="Entity1", entity_type="test"),
|
||||
Entity(name="Entity2", entity_type="test")
|
||||
]
|
||||
)
|
||||
entities = await create_entities(entity_request)
|
||||
|
||||
# Get all activity
|
||||
result = await get_recent_activity()
|
||||
|
||||
# Should find all changes
|
||||
assert result.summary.document_changes == 1
|
||||
assert result.summary.entity_changes == 2
|
||||
assert result.summary.relation_changes == 0 # No relations created
|
||||
|
||||
# Changes should be the right types
|
||||
doc_changes = [c for c in result.changes if c.activity_type == ActivityType.DOCUMENT]
|
||||
entity_changes = [c for c in result.changes if c.activity_type == ActivityType.ENTITY]
|
||||
|
||||
assert len(doc_changes) == 1
|
||||
assert len(entity_changes) == 2
|
||||
@@ -1,75 +1,102 @@
|
||||
"""Test activity service."""
|
||||
|
||||
import os.path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.services.activity_service import ActivityService
|
||||
from basic_memory.services.document_service import DocumentService
|
||||
from basic_memory.services.entity_service import EntityService
|
||||
from basic_memory.services.relation_service import RelationService
|
||||
from basic_memory.repository.document_repository import DocumentRepository
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
|
||||
|
||||
async def create_test_document(
|
||||
service: DocumentService, name: str, created_delta: timedelta, updated_delta: timedelta
|
||||
):
|
||||
"""Helper to create document with specific timestamps."""
|
||||
now = datetime.now(timezone.utc)
|
||||
doc = await service.create_document(
|
||||
path_id=f"test/{name}.md",
|
||||
content=f"Content for {name}",
|
||||
metadata={
|
||||
"created": (now - created_delta).isoformat(),
|
||||
"updated": (now - updated_delta).isoformat(),
|
||||
},
|
||||
)
|
||||
return doc
|
||||
@pytest.fixture
|
||||
def test_dir():
|
||||
"""Create a temporary test directory."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
docs_dir = os.path.join(tmpdir, 'documents')
|
||||
os.makedirs(docs_dir)
|
||||
yield Path(tmpdir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def document_service(session_maker, test_dir):
|
||||
"""Create document service."""
|
||||
docs_path = test_dir / 'documents'
|
||||
repository = DocumentRepository(session_maker)
|
||||
return DocumentService(repository, docs_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entity_service(session_maker):
|
||||
"""Create entity service."""
|
||||
repository = EntityRepository(session_maker)
|
||||
return EntityService(repository)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def relation_service(session_maker):
|
||||
"""Create relation service."""
|
||||
repository = RelationRepository(session_maker)
|
||||
return RelationService(repository)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def activity_service(document_service, entity_service, relation_service):
|
||||
"""Create activity service with real dependencies."""
|
||||
return ActivityService(entity_service, document_service, relation_service)
|
||||
|
||||
|
||||
async def create_test_document(service: DocumentService, name: str, content: str = "") -> str:
|
||||
"""Create a test document."""
|
||||
path_id = f"test/{name}.md"
|
||||
doc_content = content or f"Content for {name}"
|
||||
return await service.create_document(path_id, doc_content)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_all_types(activity_service, document_service):
|
||||
"""Test getting recent activity for all types."""
|
||||
# Create test documents with various timestamps
|
||||
test_docs = [
|
||||
await create_test_document(
|
||||
document_service, "doc1", timedelta(hours=2), timedelta(hours=2)
|
||||
),
|
||||
await create_test_document(document_service, "doc2", timedelta(days=2), timedelta(hours=1)),
|
||||
await create_test_document(
|
||||
document_service,
|
||||
"doc3",
|
||||
timedelta(days=3),
|
||||
timedelta(days=3), # This one should be too old
|
||||
),
|
||||
]
|
||||
"""Test finding recently created documents."""
|
||||
# Create test documents
|
||||
doc1 = await create_test_document(document_service, "doc1")
|
||||
doc2 = await create_test_document(document_service, "doc2")
|
||||
|
||||
# Get activity from last day
|
||||
result = await activity_service.get_recent_activity(timeframe="1d")
|
||||
|
||||
# Verify results
|
||||
assert len(result.changes) == 2 # Should only find 2 recent docs
|
||||
# Should find both docs
|
||||
assert len(result.changes) == 2
|
||||
assert result.summary.document_changes == 2
|
||||
assert result.summary.entity_changes == 0
|
||||
assert result.summary.relation_changes == 0
|
||||
|
||||
# Verify changes are sorted by timestamp (most recent first)
|
||||
# Changes should be sorted by timestamp (most recent first)
|
||||
timestamps = [change.timestamp for change in result.changes]
|
||||
assert timestamps == sorted(timestamps, reverse=True)
|
||||
|
||||
# Check specific document paths
|
||||
paths = {change.path_id for change in result.changes}
|
||||
assert "test/doc1.md" in paths
|
||||
assert "test/doc2.md" in paths
|
||||
assert "test/doc3.md" not in paths # Too old to be included
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_filtered_types(activity_service, document_service):
|
||||
"""Test getting recent activity with type filtering."""
|
||||
# Create some test documents
|
||||
await create_test_document(
|
||||
document_service, "filtered_doc", timedelta(hours=1), timedelta(hours=1)
|
||||
)
|
||||
"""Test activity filtering by type."""
|
||||
# Create test document
|
||||
await create_test_document(document_service, "filtered_doc")
|
||||
|
||||
# Get activity filtered to only documents
|
||||
result = await activity_service.get_recent_activity(timeframe="1d", activity_types=["document"])
|
||||
result = await activity_service.get_recent_activity(
|
||||
timeframe="1d",
|
||||
activity_types=["document"]
|
||||
)
|
||||
|
||||
# Verify results
|
||||
# Should find our test doc
|
||||
assert len(result.changes) == 1
|
||||
assert result.summary.document_changes == 1
|
||||
assert result.summary.entity_changes == 0
|
||||
@@ -79,44 +106,19 @@ async def test_get_recent_activity_filtered_types(activity_service, document_ser
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_activity_without_content(activity_service, document_service):
|
||||
"""Test getting activity without content."""
|
||||
# Create test document
|
||||
# Create test document with content
|
||||
await create_test_document(
|
||||
document_service, "no_content_doc", timedelta(hours=1), timedelta(hours=1)
|
||||
document_service,
|
||||
"no_content_doc",
|
||||
content="This content should not appear"
|
||||
)
|
||||
|
||||
# Get activity without content
|
||||
result = await activity_service.get_recent_activity(timeframe="1d", include_content=False)
|
||||
result = await activity_service.get_recent_activity(
|
||||
timeframe="1d",
|
||||
include_content=False
|
||||
)
|
||||
|
||||
# Verify results
|
||||
# Should find doc but without content
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].content is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_type_detection(activity_service, document_service):
|
||||
"""Test correct detection of created vs updated changes."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Create documents with different creation/update patterns
|
||||
new_doc = await create_test_document(
|
||||
document_service,
|
||||
"new_doc",
|
||||
timedelta(hours=1), # Created recently
|
||||
timedelta(hours=1), # Updated recently
|
||||
)
|
||||
|
||||
updated_doc = await create_test_document(
|
||||
document_service,
|
||||
"updated_doc",
|
||||
timedelta(days=2), # Created a while ago
|
||||
timedelta(hours=2), # But updated recently
|
||||
)
|
||||
|
||||
# Get activity
|
||||
result = await activity_service.get_recent_activity(timeframe="1d")
|
||||
|
||||
# Verify change types
|
||||
changes_by_path = {change.path_id: change for change in result.changes}
|
||||
|
||||
assert changes_by_path["test/new_doc.md"].change_type == "created"
|
||||
assert changes_by_path["test/updated_doc.md"].change_type == "updated"
|
||||
assert result.changes[0].content is None
|
||||
Reference in New Issue
Block a user