Skip to content

03. Backend Deep Dive

Layout and entrypoints

Path Role
backend/app/main.py FastAPI app, CORS, lifespan create_all, router mount
backend/app/config.py Pydantic Settings (../.env)
backend/app/core/ Ingestion, extraction, OCR, LLM, audit, sanitize
backend/app/uc1_factsheet/ Factsheet API + phased services
backend/app/workers/ Celery app, async bridge, tasks
backend/app/prompts/ Jinja prompt pack
backend/app/db/ SQLAlchemy models + async session

Run via Makefile: uvicorn app.main:app from backend/ (default port 8000).

API surface (/api/v1)

Documents (core/ingestion/router.py)

Method Path Behavior
POST /documents Upload PDF → MinIO + DB → enqueue pipeline
GET /documents List
GET /documents/{id}/status Status
GET /documents/{id}/artifacts Artifact list
GET /documents/{id}/ocr-texts OCR markdown artifacts
GET /documents/{id}/extracted-info Merged extraction
DELETE /documents/{id} Delete document + related data

Jobs (core/extraction/router.py)

Method Path Behavior
POST /jobs/{id}/start Start / continue pipeline
POST /jobs/{id}/resume Resume after partial failure
POST /jobs/{id}/run-ocr Re-run OCR stage
POST /jobs/{id}/run-extraction Re-run extraction
POST /jobs/{id}/run-merge Re-run merge
GET /jobs List
GET /jobs/{id} Detail
GET /jobs/{id}/chunks Chunk statuses

Factsheet (uc1_factsheet/router.py)

Method Path Behavior
POST /factsheet/generate 202 Accepted; Celery task
GET /factsheet/status/{job_id} Celery / record status
GET /factsheet List
GET /factsheet/{id} Detail
DELETE /factsheet/{id} Delete

Health: GET /health on the app root.

Request lifecycle

  1. Router validates input and opens AsyncSession via Depends(get_db).
  2. Ingestion writes object to S3-compatible storage and creates Document / DocumentJob.
  3. Celery task runs stages; each stage updates JobStage / JobChunk and writes Artifact rows.
  4. Clients poll document/job endpoints; UI also polls factsheet status by Celery task_id.

No auth dependency is attached to these routers (Confirmed).

Pipeline and concurrency

workers/tasks/pipeline_tasks.py (~900 LOC) orchestrates:

  1. OCR — page-range chunks (OCR_PAGE_CHUNK_SIZE), parallel calls capped by OCR_MAX_PARALLEL_CALLS
  2. EXTRACTION — text-span chunks, parallel LLM extraction
  3. MERGE — normalize into entities / attributes / relationships

Retries: JOB_CHUNK_MAX_RETRIES. Failed chunks can leave job PARTIAL.

Celery tasks are sync; async SQLAlchemy and LLM calls go through workers/async_runner.py:

run_async_in_worker_loop(coro) → dedicated process event loop → run_until_complete

OCR engines

OCR_ENGINE in settings selects:

Value Adapter Notes
vlm (default) PyMuPDF pages → LiteLLM vision Uses VLLM_MODEL
sarvam Sarvam OCR async API Poll interval / max wait settings
layout HTTP client to Paddle OCR service PADDLE_OCR_SERVICE_URL (often local ocr-service)

Factory lives under core/ocr/. Language default en-IN.

LLM client and prompts

  • core/llm/client.py wraps LiteLLM with timeout / max tokens from settings
  • Prompts under prompts/ via prompt_manager.py (OCR, extraction, factsheet)
  • core/text/sanitize.py strips oversized base64 images before LLM context

UC1 factsheet service

Phase Module Role
1 service/phase1.py Discriminators / field selection; honor prefilled
2 service/phase2.py Per-doc sub-model extraction; entities-first then OCR fallback when confidence < FACTSHEET_CONFIDENCE_THRESHOLD (0.6)
3 service/factsheet.py Assemble result + provenance

Supporting assets: factsheet_manifest.json, Pydantic models in uc1_factsheet/models.py.

Configuration highlights

From config.py (not exhaustive):

  • DATABASE_URL, REDIS_URL
  • S3_ENDPOINT_URL, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_DOCS
  • LLM_MODEL, VLLM_MODEL, OPENAI_*
  • Chunking / parallelism knobs
  • FACTSHEET_PHASE1_MAX_CHARS, FACTSHEET_PHASE2_MAX_CHARS
  • OIDC_*, JWT_SECRET (unused on routes)

.env.example documents MINIO_* names that do not match S3_* settings fields — see 06.

Auth helpers (unused)

dependencies.py defines JWT validation, a dev-token bypass, and require_role. Routers do not depend on them. Treat as scaffolding for a future SSO cutover.

Error handling and logging

  • LoggingMiddleware on all requests
  • setup_logging() from settings LOG_LEVEL
  • Job/chunk last_error and factsheet error_message persist failure text
  • HTTP errors via FastAPI defaults; no global domain exception taxonomy beyond routers

Backend interview Q&A

Q: Why Celery instead of asyncio background tasks?
A: Long OCR/LLM work needs process isolation, retries, and horizontal workers. FastAPI stays responsive; Redis brokers durable tasks.

Q: How do you resume a failed job?
A: Stages and chunks are rows with status. resume / run-* endpoints re-enqueue only the needed stage rather than re-uploading the PDF.

Q: How does async code run inside Celery?
A: A process-local event loop in async_runner.py runs coroutines to completion so asyncpg and async LLM clients work from sync task bodies.