Add OpenMemory (#2676)
Co-authored-by: Saket Aryan <94069182+whysosaket@users.noreply.github.com> Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
5
openmemory/api/app/routers/__init__.py
Normal file
5
openmemory/api/app/routers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .memories import router as memories_router
|
||||
from .apps import router as apps_router
|
||||
from .stats import router as stats_router
|
||||
|
||||
__all__ = ["memories_router", "apps_router", "stats_router"]
|
||||
223
openmemory/api/app/routers/apps.py
Normal file
223
openmemory/api/app/routers/apps.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import func, desc
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import App, Memory, MemoryAccessLog, MemoryState
|
||||
|
||||
router = APIRouter(prefix="/api/v1/apps", tags=["apps"])
|
||||
|
||||
# Helper functions
|
||||
def get_app_or_404(db: Session, app_id: UUID) -> App:
|
||||
app = db.query(App).filter(App.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
return app
|
||||
|
||||
# List all apps with filtering
|
||||
@router.get("/")
|
||||
async def list_apps(
|
||||
name: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
sort_by: str = 'name',
|
||||
sort_direction: str = 'asc',
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Create a subquery for memory counts
|
||||
memory_counts = db.query(
|
||||
Memory.app_id,
|
||||
func.count(Memory.id).label('memory_count')
|
||||
).filter(
|
||||
Memory.state.in_([MemoryState.active, MemoryState.paused, MemoryState.archived])
|
||||
).group_by(Memory.app_id).subquery()
|
||||
|
||||
# Create a subquery for access counts
|
||||
access_counts = db.query(
|
||||
MemoryAccessLog.app_id,
|
||||
func.count(func.distinct(MemoryAccessLog.memory_id)).label('access_count')
|
||||
).group_by(MemoryAccessLog.app_id).subquery()
|
||||
|
||||
# Base query
|
||||
query = db.query(
|
||||
App,
|
||||
func.coalesce(memory_counts.c.memory_count, 0).label('total_memories_created'),
|
||||
func.coalesce(access_counts.c.access_count, 0).label('total_memories_accessed')
|
||||
)
|
||||
|
||||
# Join with subqueries
|
||||
query = query.outerjoin(
|
||||
memory_counts,
|
||||
App.id == memory_counts.c.app_id
|
||||
).outerjoin(
|
||||
access_counts,
|
||||
App.id == access_counts.c.app_id
|
||||
)
|
||||
|
||||
if name:
|
||||
query = query.filter(App.name.ilike(f"%{name}%"))
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(App.is_active == is_active)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by == 'name':
|
||||
sort_field = App.name
|
||||
elif sort_by == 'memories':
|
||||
sort_field = func.coalesce(memory_counts.c.memory_count, 0)
|
||||
elif sort_by == 'memories_accessed':
|
||||
sort_field = func.coalesce(access_counts.c.access_count, 0)
|
||||
else:
|
||||
sort_field = App.name # default sort
|
||||
|
||||
if sort_direction == 'desc':
|
||||
query = query.order_by(desc(sort_field))
|
||||
else:
|
||||
query = query.order_by(sort_field)
|
||||
|
||||
total = query.count()
|
||||
apps = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"apps": [
|
||||
{
|
||||
"id": app[0].id,
|
||||
"name": app[0].name,
|
||||
"is_active": app[0].is_active,
|
||||
"total_memories_created": app[1],
|
||||
"total_memories_accessed": app[2]
|
||||
}
|
||||
for app in apps
|
||||
]
|
||||
}
|
||||
|
||||
# Get app details
|
||||
@router.get("/{app_id}")
|
||||
async def get_app_details(
|
||||
app_id: UUID,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
app = get_app_or_404(db, app_id)
|
||||
|
||||
# Get memory access statistics
|
||||
access_stats = db.query(
|
||||
func.count(MemoryAccessLog.id).label("total_memories_accessed"),
|
||||
func.min(MemoryAccessLog.accessed_at).label("first_accessed"),
|
||||
func.max(MemoryAccessLog.accessed_at).label("last_accessed")
|
||||
).filter(MemoryAccessLog.app_id == app_id).first()
|
||||
|
||||
return {
|
||||
"is_active": app.is_active,
|
||||
"total_memories_created": db.query(Memory)
|
||||
.filter(Memory.app_id == app_id)
|
||||
.count(),
|
||||
"total_memories_accessed": access_stats.total_memories_accessed or 0,
|
||||
"first_accessed": access_stats.first_accessed,
|
||||
"last_accessed": access_stats.last_accessed
|
||||
}
|
||||
|
||||
# List memories created by app
|
||||
@router.get("/{app_id}/memories")
|
||||
async def list_app_memories(
|
||||
app_id: UUID,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
get_app_or_404(db, app_id)
|
||||
query = db.query(Memory).filter(
|
||||
Memory.app_id == app_id,
|
||||
Memory.state.in_([MemoryState.active, MemoryState.paused, MemoryState.archived])
|
||||
)
|
||||
# Add eager loading for categories
|
||||
query = query.options(joinedload(Memory.categories))
|
||||
total = query.count()
|
||||
memories = query.order_by(Memory.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"memories": [
|
||||
{
|
||||
"id": memory.id,
|
||||
"content": memory.content,
|
||||
"created_at": memory.created_at,
|
||||
"state": memory.state.value,
|
||||
"app_id": memory.app_id,
|
||||
"categories": [category.name for category in memory.categories],
|
||||
"metadata_": memory.metadata_
|
||||
}
|
||||
for memory in memories
|
||||
]
|
||||
}
|
||||
|
||||
# List memories accessed by app
|
||||
@router.get("/{app_id}/accessed")
|
||||
async def list_app_accessed_memories(
|
||||
app_id: UUID,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
|
||||
# Get memories with access counts
|
||||
query = db.query(
|
||||
Memory,
|
||||
func.count(MemoryAccessLog.id).label("access_count")
|
||||
).join(
|
||||
MemoryAccessLog,
|
||||
Memory.id == MemoryAccessLog.memory_id
|
||||
).filter(
|
||||
MemoryAccessLog.app_id == app_id
|
||||
).group_by(
|
||||
Memory.id
|
||||
).order_by(
|
||||
desc("access_count")
|
||||
)
|
||||
|
||||
# Add eager loading for categories
|
||||
query = query.options(joinedload(Memory.categories))
|
||||
|
||||
total = query.count()
|
||||
results = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"memories": [
|
||||
{
|
||||
"memory": {
|
||||
"id": memory.id,
|
||||
"content": memory.content,
|
||||
"created_at": memory.created_at,
|
||||
"state": memory.state.value,
|
||||
"app_id": memory.app_id,
|
||||
"app_name": memory.app.name if memory.app else None,
|
||||
"categories": [category.name for category in memory.categories],
|
||||
"metadata_": memory.metadata_
|
||||
},
|
||||
"access_count": count
|
||||
}
|
||||
for memory, count in results
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{app_id}")
|
||||
async def update_app_details(
|
||||
app_id: UUID,
|
||||
is_active: bool,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
app = get_app_or_404(db, app_id)
|
||||
app.is_active = is_active
|
||||
db.commit()
|
||||
return {"status": "success", "message": "Updated app details successfully"}
|
||||
575
openmemory/api/app/routers/memories.py
Normal file
575
openmemory/api/app/routers/memories.py
Normal file
@@ -0,0 +1,575 @@
|
||||
from datetime import datetime, UTC
|
||||
from typing import List, Optional, Set
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from fastapi_pagination import Page, Params
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate as sqlalchemy_paginate
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import or_, func
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import (
|
||||
Memory, MemoryState, MemoryAccessLog, App,
|
||||
MemoryStatusHistory, User, Category, AccessControl
|
||||
)
|
||||
from app.schemas import MemoryResponse, PaginatedMemoryResponse
|
||||
from app.utils.permissions import check_memory_access_permissions
|
||||
|
||||
router = APIRouter(prefix="/api/v1/memories", tags=["memories"])
|
||||
|
||||
|
||||
def get_memory_or_404(db: Session, memory_id: UUID) -> Memory:
|
||||
memory = db.query(Memory).filter(Memory.id == memory_id).first()
|
||||
if not memory:
|
||||
raise HTTPException(status_code=404, detail="Memory not found")
|
||||
return memory
|
||||
|
||||
|
||||
def update_memory_state(db: Session, memory_id: UUID, new_state: MemoryState, user_id: UUID):
|
||||
memory = get_memory_or_404(db, memory_id)
|
||||
old_state = memory.state
|
||||
|
||||
# Update memory state
|
||||
memory.state = new_state
|
||||
if new_state == MemoryState.archived:
|
||||
memory.archived_at = datetime.now(UTC)
|
||||
elif new_state == MemoryState.deleted:
|
||||
memory.deleted_at = datetime.now(UTC)
|
||||
|
||||
# Record state change
|
||||
history = MemoryStatusHistory(
|
||||
memory_id=memory_id,
|
||||
changed_by=user_id,
|
||||
old_state=old_state,
|
||||
new_state=new_state
|
||||
)
|
||||
db.add(history)
|
||||
db.commit()
|
||||
return memory
|
||||
|
||||
|
||||
def get_accessible_memory_ids(db: Session, app_id: UUID) -> Set[UUID]:
|
||||
"""
|
||||
Get the set of memory IDs that the app has access to based on app-level ACL rules.
|
||||
Returns all memory IDs if no specific restrictions are found.
|
||||
"""
|
||||
# Get app-level access controls
|
||||
app_access = db.query(AccessControl).filter(
|
||||
AccessControl.subject_type == "app",
|
||||
AccessControl.subject_id == app_id,
|
||||
AccessControl.object_type == "memory"
|
||||
).all()
|
||||
|
||||
# If no app-level rules exist, return None to indicate all memories are accessible
|
||||
if not app_access:
|
||||
return None
|
||||
|
||||
# Initialize sets for allowed and denied memory IDs
|
||||
allowed_memory_ids = set()
|
||||
denied_memory_ids = set()
|
||||
|
||||
# Process app-level rules
|
||||
for rule in app_access:
|
||||
if rule.effect == "allow":
|
||||
if rule.object_id: # Specific memory access
|
||||
allowed_memory_ids.add(rule.object_id)
|
||||
else: # All memories access
|
||||
return None # All memories allowed
|
||||
elif rule.effect == "deny":
|
||||
if rule.object_id: # Specific memory denied
|
||||
denied_memory_ids.add(rule.object_id)
|
||||
else: # All memories denied
|
||||
return set() # No memories accessible
|
||||
|
||||
# Remove denied memories from allowed set
|
||||
if allowed_memory_ids:
|
||||
allowed_memory_ids -= denied_memory_ids
|
||||
|
||||
return allowed_memory_ids
|
||||
|
||||
|
||||
# List all memories with filtering
|
||||
@router.get("/", response_model=Page[MemoryResponse])
|
||||
async def list_memories(
|
||||
user_id: str,
|
||||
app_id: Optional[UUID] = None,
|
||||
from_date: Optional[int] = Query(
|
||||
None,
|
||||
description="Filter memories created after this date (timestamp)",
|
||||
examples=[1718505600]
|
||||
),
|
||||
to_date: Optional[int] = Query(
|
||||
None,
|
||||
description="Filter memories created before this date (timestamp)",
|
||||
examples=[1718505600]
|
||||
),
|
||||
categories: Optional[str] = None,
|
||||
params: Params = Depends(),
|
||||
search_query: Optional[str] = None,
|
||||
sort_column: Optional[str] = Query(None, description="Column to sort by (memory, categories, app_name, created_at)"),
|
||||
sort_direction: Optional[str] = Query(None, description="Sort direction (asc or desc)"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Build base query
|
||||
query = db.query(Memory).filter(
|
||||
Memory.user_id == user.id,
|
||||
Memory.state != MemoryState.deleted,
|
||||
Memory.state != MemoryState.archived,
|
||||
Memory.content.ilike(f"%{search_query}%") if search_query else True
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if app_id:
|
||||
query = query.filter(Memory.app_id == app_id)
|
||||
|
||||
if from_date:
|
||||
from_datetime = datetime.fromtimestamp(from_date, tz=UTC)
|
||||
query = query.filter(Memory.created_at >= from_datetime)
|
||||
|
||||
if to_date:
|
||||
to_datetime = datetime.fromtimestamp(to_date, tz=UTC)
|
||||
query = query.filter(Memory.created_at <= to_datetime)
|
||||
|
||||
# Add joins for app and categories after filtering
|
||||
query = query.outerjoin(App, Memory.app_id == App.id)
|
||||
query = query.outerjoin(Memory.categories)
|
||||
|
||||
# Apply category filter if provided
|
||||
if categories:
|
||||
category_list = [c.strip() for c in categories.split(",")]
|
||||
query = query.filter(Category.name.in_(category_list))
|
||||
|
||||
# Apply sorting if specified
|
||||
if sort_column:
|
||||
sort_field = getattr(Memory, sort_column, None)
|
||||
if sort_field:
|
||||
query = query.order_by(sort_field.desc()) if sort_direction == "desc" else query.order_by(sort_field.asc())
|
||||
|
||||
|
||||
# Get paginated results
|
||||
paginated_results = sqlalchemy_paginate(query, params)
|
||||
|
||||
# Filter results based on permissions
|
||||
filtered_items = []
|
||||
for item in paginated_results.items:
|
||||
if check_memory_access_permissions(db, item, app_id):
|
||||
filtered_items.append(item)
|
||||
|
||||
# Update paginated results with filtered items
|
||||
paginated_results.items = filtered_items
|
||||
paginated_results.total = len(filtered_items)
|
||||
|
||||
return paginated_results
|
||||
|
||||
|
||||
# Get all categories
|
||||
@router.get("/categories")
|
||||
async def get_categories(
|
||||
user_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Get unique categories associated with the user's memories
|
||||
# Get all memories
|
||||
memories = db.query(Memory).filter(Memory.user_id == user.id, Memory.state != MemoryState.deleted, Memory.state != MemoryState.archived).all()
|
||||
# Get all categories from memories
|
||||
categories = [category for memory in memories for category in memory.categories]
|
||||
# Get unique categories
|
||||
unique_categories = list(set(categories))
|
||||
|
||||
return {
|
||||
"categories": unique_categories,
|
||||
"total": len(unique_categories)
|
||||
}
|
||||
|
||||
|
||||
class CreateMemoryRequest(BaseModel):
|
||||
user_id: str
|
||||
text: str
|
||||
metadata: dict = {}
|
||||
infer: bool = True
|
||||
app: str = "openmemory"
|
||||
|
||||
|
||||
# Create new memory
|
||||
@router.post("/")
|
||||
async def create_memory(
|
||||
request: CreateMemoryRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == request.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
# Get or create app
|
||||
app_obj = db.query(App).filter(App.name == request.app).first()
|
||||
if not app_obj:
|
||||
app_obj = App(name=request.app, owner_id=user.id)
|
||||
db.add(app_obj)
|
||||
db.commit()
|
||||
db.refresh(app_obj)
|
||||
|
||||
# Check if app is active
|
||||
if not app_obj.is_active:
|
||||
raise HTTPException(status_code=403, detail=f"App {request.app} is currently paused on OpenMemory. Cannot create new memories.")
|
||||
|
||||
# Create memory
|
||||
memory = Memory(
|
||||
user_id=user.id,
|
||||
app_id=app_obj.id,
|
||||
content=request.text,
|
||||
metadata_=request.metadata
|
||||
)
|
||||
db.add(memory)
|
||||
db.commit()
|
||||
db.refresh(memory)
|
||||
return memory
|
||||
|
||||
|
||||
# Get memory by ID
|
||||
@router.get("/{memory_id}")
|
||||
async def get_memory(
|
||||
memory_id: UUID,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
memory = get_memory_or_404(db, memory_id)
|
||||
return {
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"created_at": int(memory.created_at.timestamp()),
|
||||
"state": memory.state.value,
|
||||
"app_id": memory.app_id,
|
||||
"app_name": memory.app.name if memory.app else None,
|
||||
"categories": [category.name for category in memory.categories],
|
||||
"metadata_": memory.metadata_
|
||||
}
|
||||
|
||||
|
||||
class DeleteMemoriesRequest(BaseModel):
|
||||
memory_ids: List[UUID]
|
||||
user_id: str
|
||||
|
||||
# Delete multiple memories
|
||||
@router.delete("/")
|
||||
async def delete_memories(
|
||||
request: DeleteMemoriesRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == request.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
for memory_id in request.memory_ids:
|
||||
update_memory_state(db, memory_id, MemoryState.deleted, user.id)
|
||||
return {"message": f"Successfully deleted {len(request.memory_ids)} memories"}
|
||||
|
||||
|
||||
# Archive memories
|
||||
@router.post("/actions/archive")
|
||||
async def archive_memories(
|
||||
memory_ids: List[UUID],
|
||||
user_id: UUID,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
for memory_id in memory_ids:
|
||||
update_memory_state(db, memory_id, MemoryState.archived, user_id)
|
||||
return {"message": f"Successfully archived {len(memory_ids)} memories"}
|
||||
|
||||
|
||||
class PauseMemoriesRequest(BaseModel):
|
||||
memory_ids: Optional[List[UUID]] = None
|
||||
category_ids: Optional[List[UUID]] = None
|
||||
app_id: Optional[UUID] = None
|
||||
all_for_app: bool = False
|
||||
global_pause: bool = False
|
||||
state: Optional[MemoryState] = None
|
||||
user_id: str
|
||||
|
||||
# Pause access to memories
|
||||
@router.post("/actions/pause")
|
||||
async def pause_memories(
|
||||
request: PauseMemoriesRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
|
||||
global_pause = request.global_pause
|
||||
all_for_app = request.all_for_app
|
||||
app_id = request.app_id
|
||||
memory_ids = request.memory_ids
|
||||
category_ids = request.category_ids
|
||||
state = request.state or MemoryState.paused
|
||||
|
||||
user = db.query(User).filter(User.user_id == request.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user_id = user.id
|
||||
|
||||
if global_pause:
|
||||
# Pause all memories
|
||||
memories = db.query(Memory).filter(
|
||||
Memory.state != MemoryState.deleted,
|
||||
Memory.state != MemoryState.archived
|
||||
).all()
|
||||
for memory in memories:
|
||||
update_memory_state(db, memory.id, state, user_id)
|
||||
return {"message": "Successfully paused all memories"}
|
||||
|
||||
if app_id:
|
||||
# Pause all memories for an app
|
||||
memories = db.query(Memory).filter(
|
||||
Memory.app_id == app_id,
|
||||
Memory.user_id == user.id,
|
||||
Memory.state != MemoryState.deleted,
|
||||
Memory.state != MemoryState.archived
|
||||
).all()
|
||||
for memory in memories:
|
||||
update_memory_state(db, memory.id, state, user_id)
|
||||
return {"message": f"Successfully paused all memories for app {app_id}"}
|
||||
|
||||
if all_for_app and memory_ids:
|
||||
# Pause all memories for an app
|
||||
memories = db.query(Memory).filter(
|
||||
Memory.user_id == user.id,
|
||||
Memory.state != MemoryState.deleted,
|
||||
Memory.id.in_(memory_ids)
|
||||
).all()
|
||||
for memory in memories:
|
||||
update_memory_state(db, memory.id, state, user_id)
|
||||
return {"message": f"Successfully paused all memories"}
|
||||
|
||||
if memory_ids:
|
||||
# Pause specific memories
|
||||
for memory_id in memory_ids:
|
||||
update_memory_state(db, memory_id, state, user_id)
|
||||
return {"message": f"Successfully paused {len(memory_ids)} memories"}
|
||||
|
||||
if category_ids:
|
||||
# Pause memories by category
|
||||
memories = db.query(Memory).join(Memory.categories).filter(
|
||||
Category.id.in_(category_ids),
|
||||
Memory.state != MemoryState.deleted,
|
||||
Memory.state != MemoryState.archived
|
||||
).all()
|
||||
for memory in memories:
|
||||
update_memory_state(db, memory.id, state, user_id)
|
||||
return {"message": f"Successfully paused memories in {len(category_ids)} categories"}
|
||||
|
||||
raise HTTPException(status_code=400, detail="Invalid pause request parameters")
|
||||
|
||||
|
||||
# Get memory access logs
|
||||
@router.get("/{memory_id}/access-log")
|
||||
async def get_memory_access_log(
|
||||
memory_id: UUID,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
query = db.query(MemoryAccessLog).filter(MemoryAccessLog.memory_id == memory_id)
|
||||
total = query.count()
|
||||
logs = query.order_by(MemoryAccessLog.accessed_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# Get app name
|
||||
for log in logs:
|
||||
app = db.query(App).filter(App.id == log.app_id).first()
|
||||
log.app_name = app.name if app else None
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"logs": logs
|
||||
}
|
||||
|
||||
|
||||
class UpdateMemoryRequest(BaseModel):
|
||||
memory_content: str
|
||||
user_id: str
|
||||
|
||||
# Update a memory
|
||||
@router.put("/{memory_id}")
|
||||
async def update_memory(
|
||||
memory_id: UUID,
|
||||
request: UpdateMemoryRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == request.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
memory = get_memory_or_404(db, memory_id)
|
||||
memory.content = request.memory_content
|
||||
db.commit()
|
||||
db.refresh(memory)
|
||||
return memory
|
||||
|
||||
class FilterMemoriesRequest(BaseModel):
|
||||
user_id: str
|
||||
page: int = 1
|
||||
size: int = 10
|
||||
search_query: Optional[str] = None
|
||||
app_ids: Optional[List[UUID]] = None
|
||||
category_ids: Optional[List[UUID]] = None
|
||||
sort_column: Optional[str] = None
|
||||
sort_direction: Optional[str] = None
|
||||
from_date: Optional[int] = None
|
||||
to_date: Optional[int] = None
|
||||
show_archived: Optional[bool] = False
|
||||
|
||||
@router.post("/filter", response_model=Page[MemoryResponse])
|
||||
async def filter_memories(
|
||||
request: FilterMemoriesRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == request.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Build base query
|
||||
query = db.query(Memory).filter(
|
||||
Memory.user_id == user.id,
|
||||
Memory.state != MemoryState.deleted,
|
||||
)
|
||||
|
||||
# Filter archived memories based on show_archived parameter
|
||||
if not request.show_archived:
|
||||
query = query.filter(Memory.state != MemoryState.archived)
|
||||
|
||||
# Apply search filter
|
||||
if request.search_query:
|
||||
query = query.filter(Memory.content.ilike(f"%{request.search_query}%"))
|
||||
|
||||
# Apply app filter
|
||||
if request.app_ids:
|
||||
query = query.filter(Memory.app_id.in_(request.app_ids))
|
||||
|
||||
# Add joins for app and categories
|
||||
query = query.outerjoin(App, Memory.app_id == App.id)
|
||||
|
||||
# Apply category filter
|
||||
if request.category_ids:
|
||||
query = query.join(Memory.categories).filter(Category.id.in_(request.category_ids))
|
||||
else:
|
||||
query = query.outerjoin(Memory.categories)
|
||||
|
||||
# Apply date filters
|
||||
if request.from_date:
|
||||
from_datetime = datetime.fromtimestamp(request.from_date, tz=UTC)
|
||||
query = query.filter(Memory.created_at >= from_datetime)
|
||||
|
||||
if request.to_date:
|
||||
to_datetime = datetime.fromtimestamp(request.to_date, tz=UTC)
|
||||
query = query.filter(Memory.created_at <= to_datetime)
|
||||
|
||||
# Apply sorting
|
||||
if request.sort_column and request.sort_direction:
|
||||
sort_direction = request.sort_direction.lower()
|
||||
if sort_direction not in ['asc', 'desc']:
|
||||
raise HTTPException(status_code=400, detail="Invalid sort direction")
|
||||
|
||||
sort_mapping = {
|
||||
'memory': Memory.content,
|
||||
'app_name': App.name,
|
||||
'created_at': Memory.created_at
|
||||
}
|
||||
|
||||
if request.sort_column not in sort_mapping:
|
||||
raise HTTPException(status_code=400, detail="Invalid sort column")
|
||||
|
||||
sort_field = sort_mapping[request.sort_column]
|
||||
if sort_direction == 'desc':
|
||||
query = query.order_by(sort_field.desc())
|
||||
else:
|
||||
query = query.order_by(sort_field.asc())
|
||||
else:
|
||||
# Default sorting
|
||||
query = query.order_by(Memory.created_at.desc())
|
||||
|
||||
# Add eager loading for categories and make the query distinct
|
||||
query = query.options(
|
||||
joinedload(Memory.categories)
|
||||
).distinct(Memory.id)
|
||||
|
||||
# Use fastapi-pagination's paginate function
|
||||
return sqlalchemy_paginate(
|
||||
query,
|
||||
Params(page=request.page, size=request.size),
|
||||
transformer=lambda items: [
|
||||
MemoryResponse(
|
||||
id=memory.id,
|
||||
content=memory.content,
|
||||
created_at=memory.created_at,
|
||||
state=memory.state.value,
|
||||
app_id=memory.app_id,
|
||||
app_name=memory.app.name if memory.app else None,
|
||||
categories=[category.name for category in memory.categories],
|
||||
metadata_=memory.metadata_
|
||||
)
|
||||
for memory in items
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{memory_id}/related", response_model=Page[MemoryResponse])
|
||||
async def get_related_memories(
|
||||
memory_id: UUID,
|
||||
user_id: str,
|
||||
params: Params = Depends(),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Validate user
|
||||
user = db.query(User).filter(User.user_id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Get the source memory
|
||||
memory = get_memory_or_404(db, memory_id)
|
||||
|
||||
# Extract category IDs from the source memory
|
||||
category_ids = [category.id for category in memory.categories]
|
||||
|
||||
if not category_ids:
|
||||
return Page.create([], total=0, params=params)
|
||||
|
||||
# Build query for related memories
|
||||
query = db.query(Memory).distinct(Memory.id).filter(
|
||||
Memory.user_id == user.id,
|
||||
Memory.id != memory_id,
|
||||
Memory.state != MemoryState.deleted
|
||||
).join(Memory.categories).filter(
|
||||
Category.id.in_(category_ids)
|
||||
).options(
|
||||
joinedload(Memory.categories),
|
||||
joinedload(Memory.app)
|
||||
).order_by(
|
||||
func.count(Category.id).desc(),
|
||||
Memory.created_at.desc()
|
||||
).group_by(Memory.id)
|
||||
|
||||
# ⚡ Force page size to be 5
|
||||
params = Params(page=params.page, size=5)
|
||||
|
||||
return sqlalchemy_paginate(
|
||||
query,
|
||||
params,
|
||||
transformer=lambda items: [
|
||||
MemoryResponse(
|
||||
id=memory.id,
|
||||
content=memory.content,
|
||||
created_at=memory.created_at,
|
||||
state=memory.state.value,
|
||||
app_id=memory.app_id,
|
||||
app_name=memory.app.name if memory.app else None,
|
||||
categories=[category.name for category in memory.categories],
|
||||
metadata_=memory.metadata_
|
||||
)
|
||||
for memory in items
|
||||
]
|
||||
)
|
||||
30
openmemory/api/app/routers/stats.py
Normal file
30
openmemory/api/app/routers/stats.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import User, Memory, App, MemoryState
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1/stats", tags=["stats"])
|
||||
|
||||
@router.get("/")
|
||||
async def get_profile(
|
||||
user_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.user_id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Get total number of memories
|
||||
total_memories = db.query(Memory).filter(Memory.user_id == user.id, Memory.state != MemoryState.deleted).count()
|
||||
|
||||
# Get total number of apps
|
||||
apps = db.query(App).filter(App.owner == user)
|
||||
total_apps = apps.count()
|
||||
|
||||
return {
|
||||
"total_memories": total_memories,
|
||||
"total_apps": total_apps,
|
||||
"apps": apps.all()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user