aditya0103's picture
Tasks 5-7: eval harness, FastAPI backend, Paper & Ink UI- src/eval/ β€” precision/recall/F1 harness with type-aware comparators,micro/macro F1, CSV + markdown reports, --model benchmark flag- src/api/ β€” FastAPI backend with /extract, /schemas, /health,request-ID middleware, typed error envelope, injectable extractor- ui/ β€” Vite + React + TS + Tailwind + Motion + React Three FiberPaper & Ink editorial UI with 3D paper hero, dark/light mode,confidence inkwell, wax-stamp metrics, kinetic typography- 95 passing tests (up from 54); UI is a separate npm workspace
557ab38
Raw
History Blame Contribute Delete
3.04 kB
"""POST /extract β€” the main extraction endpoint.
Multipart form:
file : uploaded document (required)
doc_type : "invoice" | "receipt" (required)
model : optional model override (e.g. "gpt-5-nano", "gpt-5.4") for benchmarking
Returns:
{
"result": ExtractionResult[T] JSON,
"metrics": {input_tokens, output_tokens, latency_ms, cost_usd, model}
}
"""
from __future__ import annotations
from pathlib import Path
from fastapi import APIRouter, Depends, File, Form, UploadFile
from src.api.deps import (
ALLOWED_EXTENSIONS,
ALLOWED_MIME_TYPES,
MAX_UPLOAD_BYTES,
get_extractor,
)
from src.api.errors import (
EmptyDocument,
ExtractionFailed,
FileTooLarge,
UnsupportedDocType,
UnsupportedFileType,
)
from src.extractors.extractor import DocumentExtractor
from src.schemas.registry import get_schema
router = APIRouter(tags=["extract"])
@router.post("/extract")
async def extract(
file: UploadFile = File(..., description="PDF or image to extract from."),
doc_type: str = Form(..., description="One of the registered doc types (see GET /schemas)."),
model: str | None = Form(None, description="Optional model override (e.g. gpt-5-nano)."),
extractor: DocumentExtractor = Depends(get_extractor),
) -> dict:
# --- Validate doc_type
try:
get_schema(doc_type) # raises KeyError if unknown
except KeyError as e:
raise UnsupportedDocType(str(e), details={"doc_type": doc_type}) from e
# --- Validate file extension + content-type
filename = file.filename or "upload"
ext = Path(filename).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise UnsupportedFileType(
f"Extension {ext!r} is not supported. Allowed: {sorted(ALLOWED_EXTENSIONS)}",
details={"filename": filename, "extension": ext},
)
if file.content_type and file.content_type not in ALLOWED_MIME_TYPES:
raise UnsupportedFileType(
f"MIME type {file.content_type!r} not accepted for {filename}.",
details={"content_type": file.content_type},
)
# --- Read + size-check
payload = await file.read()
if len(payload) == 0:
raise EmptyDocument("Uploaded file is empty.")
if len(payload) > MAX_UPLOAD_BYTES:
raise FileTooLarge(
f"File is {len(payload)} bytes; max allowed is {MAX_UPLOAD_BYTES}.",
details={"size_bytes": len(payload), "max_bytes": MAX_UPLOAD_BYTES},
)
# --- Run extraction
try:
result, metrics = extractor.extract(
payload, filename=filename, doc_type=doc_type, model_override=model
)
except ValueError as e:
# Loader reports "could not load" for unknown/corrupt formats.
raise EmptyDocument(str(e)) from e
except Exception as e: # noqa: BLE001
raise ExtractionFailed(f"Model extraction failed: {e}") from e
return {
"result": result.model_dump(mode="json"),
"metrics": metrics.to_dict(),
}