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 | """API-facing error envelope and typed exceptions. | |
| Every 4xx / 5xx response from this service returns the same JSON shape: | |
| { | |
| "error": { | |
| "code": "unsupported_doc_type", | |
| "message": "Human-readable explanation.", | |
| "request_id": "abc-123", | |
| "details": {...optional...} | |
| } | |
| } | |
| Consistent shape makes clients (Streamlit, curl, external integrations) easier | |
| to write than raw FastAPI HTTPException details. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| from pydantic import BaseModel, Field | |
| class ErrorDetail(BaseModel): | |
| code: str | |
| message: str | |
| request_id: str | None = None | |
| details: dict[str, Any] = Field(default_factory=dict) | |
| class ErrorEnvelope(BaseModel): | |
| error: ErrorDetail | |
| class APIError(Exception): | |
| """Base class β subclass rather than raising HTTPException directly.""" | |
| status_code: int = 500 | |
| code: str = "internal_error" | |
| def __init__(self, message: str, *, details: dict[str, Any] | None = None): | |
| super().__init__(message) | |
| self.message = message | |
| self.details = details or {} | |
| class UnsupportedDocType(APIError): | |
| status_code = 400 | |
| code = "unsupported_doc_type" | |
| class UnsupportedFileType(APIError): | |
| status_code = 415 | |
| code = "unsupported_media_type" | |
| class FileTooLarge(APIError): | |
| status_code = 413 | |
| code = "file_too_large" | |
| class EmptyDocument(APIError): | |
| status_code = 422 | |
| code = "empty_document" | |
| class ExtractionFailed(APIError): | |
| status_code = 502 | |
| code = "extraction_failed" | |