File size: 3,398 Bytes
da6ee5e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | """Export and dashboard API."""
from __future__ import annotations
from collections import Counter
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.db import get_session
from app.models import Batch, Document
from app.schemas import DashboardStats, DocStatus, PublicSettings
from app.services.export import build_export
router = APIRouter(prefix="/api", tags=["export"])
_XLSX_MEDIA = (
"application/vnd.openxmlformats-officedocument"
".spreadsheetml.sheet"
)
@router.get("/export")
async def export_documents(
fmt: str = Query("xlsx", pattern="^(xlsx|excel|json|csv)$"),
batch_id: str | None = None,
session: AsyncSession = Depends(get_session),
) -> FileResponse:
"""Export documents as Excel, JSON, or CSV."""
stmt = select(Document).where(
Document.status.in_(
[DocStatus.READY.value, DocStatus.VALIDATED.value]
)
)
if batch_id:
stmt = stmt.where(Document.batch_id == batch_id)
result = await session.execute(stmt)
docs = list(result.scalars().all())
if not docs:
raise HTTPException(404, "No documents to export")
try:
path = build_export(docs, fmt)
except ValueError as exc:
raise HTTPException(400, str(exc)) from exc
media = {
"json": "application/json",
"csv": "text/csv",
"xlsx": _XLSX_MEDIA,
"excel": _XLSX_MEDIA,
}
return FileResponse(
path,
filename=path.name,
media_type=media.get(fmt, "application/octet-stream"),
)
@router.get("/dashboard", response_model=DashboardStats)
async def dashboard(
session: AsyncSession = Depends(get_session),
) -> DashboardStats:
"""Aggregate processing stats."""
docs = (
await session.execute(select(Document))
).scalars().all()
by_type = Counter(d.doc_type for d in docs)
needs_review = 0
conf_scores: list[float] = []
duplicates = 0
for doc in docs:
if doc.is_duplicate:
duplicates += 1
if doc.avg_confidence is not None:
conf_scores.append(doc.avg_confidence)
conf = doc.confidence or {}
if any(
isinstance(v, dict) and v.get("needs_review")
for v in conf.values()
):
needs_review += 1
batch_count = (
await session.execute(select(func.count(Batch.id)))
).scalar() or 0
avg = (
sum(conf_scores) / len(conf_scores)
if conf_scores
else 0.0
)
return DashboardStats(
total_documents=len(docs),
by_type=dict(by_type),
needs_review=needs_review,
duplicates=duplicates,
avg_confidence=round(avg, 3),
batches=batch_count,
)
@router.get("/settings/public", response_model=PublicSettings)
async def public_settings() -> PublicSettings:
"""Expose non-secret settings to the UI."""
settings = get_settings()
return PublicSettings(
app_name=settings.app_name,
confidence_threshold=settings.confidence_threshold,
output_language=settings.output_language,
openai=settings.has_openai,
text_model=settings.openai_model,
vision_model=settings.openai_vision_model,
)
|