Arag / app /api /ingest.py
AuthorBot
Fix analytics accuracy, tenant isolation, and event-loop blocking.
ff0847b
Raw
History Blame Contribute Delete
3.33 kB
"""Author RAG β€” Ingestion API Router.
Routes (per implementation plan):
POST /api/admin/{author_slug}/books/upload β€” Upload a book file
GET /api/admin/{author_slug}/books/upload/progress β€” SSE ingestion progress
Saves uploaded file to disk first, then passes the path to ingestion_service.
"""
import json
from fastapi import APIRouter, Depends, File, Query, UploadFile, HTTPException
from fastapi.responses import StreamingResponse
from app.config import get_settings
from app.dependencies import get_db, get_current_author_scoped, get_redis
from app.services.ingestion_service import start_file_upload
router = APIRouter()
cfg = get_settings()
@router.post("/{author_slug}/books/upload", status_code=202)
async def upload_book(
author_slug: str,
file: UploadFile = File(...),
title: str = "",
genre: str = "",
buy_url: str = "",
force_replace: bool = Query(False),
current_user=Depends(get_current_author_scoped),
db=Depends(get_db),
redis=Depends(get_redis),
):
"""Upload a book file (PDF/EPUB/DOCX/TXT) and start async ingestion.
MINOR-7 fix: removed 'price' param β€” start_file_upload() never accepted it
(it was silently dropped on every request).
"""
MAX_UPLOAD_BYTES = cfg.UPLOAD_MAX_FILE_SIZE_MB * 1024 * 1024
contents = bytearray()
while chunk := await file.read(64 * 1024):
contents.extend(chunk)
if len(contents) > MAX_UPLOAD_BYTES:
raise HTTPException(413, f"File too large. Maximum: {cfg.UPLOAD_MAX_FILE_SIZE_MB}MB")
filename = file.filename or "upload"
return await start_file_upload(
db=db,
author_id=current_user.id,
contents=bytes(contents),
filename=filename,
title=title,
genre=genre,
buy_url=buy_url,
redis=redis,
force_replace=force_replace,
)
@router.get("/{author_slug}/books/upload/progress")
async def ingestion_progress(
author_slug: str,
book_id: str,
current_user=Depends(get_current_author_scoped),
redis=Depends(get_redis),
):
"""SSE stream for real-time ingestion progress."""
async def event_stream():
channel = f"ingestion:{current_user.id}:{book_id}"
pubsub = redis.pubsub()
await pubsub.subscribe(channel)
try:
import time as _time
deadline = _time.monotonic() + 300
heartbeat_interval = 30
last_heartbeat = _time.monotonic()
async for message in pubsub.listen():
now = _time.monotonic()
if now >= deadline:
yield 'data: {"stage": "timeout"}\n\n'
break
if now - last_heartbeat > heartbeat_interval:
yield ": heartbeat\n\n"
last_heartbeat = now
if message["type"] == "message":
yield f"data: {message['data']}\n\n"
data = json.loads(message["data"])
if data.get("stage") in ("complete", "error"):
break
finally:
await pubsub.unsubscribe(channel)
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)