Backend:
- Document + MemoryEntry models with Alembic migration (GIN FTS index)
- File upload endpoint with path traversal protection (sanitized filenames)
- Background document text extraction (PyMuPDF)
- Full-text search on extracted_text via PostgreSQL tsvector/tsquery
- Memory CRUD with enum-validated categories/importance, field allow-list
- AI tools: save_memory, search_documents, get_memory (Claude function calling)
- Tool execution loop in stream_ai_response (multi-turn tool use)
- Context assembly: injects critical memory + relevant doc excerpts
- File storage abstraction (local filesystem, S3-swappable)
- Secure file deletion (DB flush before disk delete)
Frontend:
- Document upload dialog (drag-and-drop + file picker)
- Document list with status badges, search, download (via authenticated blob)
- Document viewer with extracted text preview
- Memory list grouped by category with importance color coding
- Memory editor with category/importance dropdowns
- Documents + Memory pages with full CRUD
- Enabled sidebar navigation for both sections
Review fixes applied:
- Sanitized upload filenames (path traversal prevention)
- Download via axios blob (not bare <a href>, preserves auth)
- Route ordering: /search before /{id}/reindex
- Memory update allows is_active=False + field allow-list
- MemoryEditor form resets on mode switch
- Literal enum validation on category/importance schemas
- DB flush before file deletion for data integrity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
import uuid
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.database import get_db
|
|
from app.models.user import User
|
|
from app.schemas.memory import (
|
|
CreateMemoryRequest,
|
|
MemoryEntryListResponse,
|
|
MemoryEntryResponse,
|
|
UpdateMemoryRequest,
|
|
)
|
|
from app.services import memory_service
|
|
|
|
router = APIRouter(prefix="/memory", tags=["memory"])
|
|
|
|
|
|
@router.post("/", response_model=MemoryEntryResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_memory(
|
|
data: CreateMemoryRequest,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
entry = await memory_service.create_memory(db, user.id, **data.model_dump())
|
|
return MemoryEntryResponse.model_validate(entry)
|
|
|
|
|
|
@router.get("/", response_model=MemoryEntryListResponse)
|
|
async def list_memories(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
category: str | None = Query(default=None),
|
|
importance: str | None = Query(default=None),
|
|
is_active: bool | None = Query(default=None),
|
|
):
|
|
entries = await memory_service.get_user_memories(db, user.id, category, importance, is_active)
|
|
return MemoryEntryListResponse(entries=[MemoryEntryResponse.model_validate(e) for e in entries])
|
|
|
|
|
|
@router.get("/{entry_id}", response_model=MemoryEntryResponse)
|
|
async def get_memory(
|
|
entry_id: uuid.UUID,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
entry = await memory_service.get_memory(db, entry_id, user.id)
|
|
return MemoryEntryResponse.model_validate(entry)
|
|
|
|
|
|
@router.patch("/{entry_id}", response_model=MemoryEntryResponse)
|
|
async def update_memory(
|
|
entry_id: uuid.UUID,
|
|
data: UpdateMemoryRequest,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
entry = await memory_service.update_memory(db, entry_id, user.id, **data.model_dump(exclude_unset=True))
|
|
return MemoryEntryResponse.model_validate(entry)
|
|
|
|
|
|
@router.delete("/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_memory(
|
|
entry_id: uuid.UUID,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
await memory_service.delete_memory(db, entry_id, user.id)
|