Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ surya-env
.DS_Store
__pycache__
.claude
CLAUDE.md
11 changes: 7 additions & 4 deletions lending-poc/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
**/surya-env
**/node_modules
**/__pycache__
document_processing
field_mapping_poc
gateway
# document_processing/, field_mapping_poc/, gateway/, and scripts/ are all
# part of the root build context now — the root Dockerfile builds the single
# combined backend image (all five services) rather than just app/, so their
# requirements.txt, source, and the entrypoint script all have to be
# reachable from here. Only frontend/ and docs/ stay out: frontend has its
# own image, docs ship nothing.
frontend
scripts
docs
*.log
.env
1 change: 1 addition & 0 deletions lending-poc/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ OLLAMA_MODEL=gemma4:e4b-it-qat
# OCR_REQUEST_TIMEOUT_SECONDS=300
# TRANSLATION_REQUEST_TIMEOUT_SECONDS=300
# FIELD_MAPPING_REQUEST_TIMEOUT_SECONDS=300
# APP_REQUEST_TIMEOUT_SECONDS=300
66 changes: 61 additions & 5 deletions lending-poc/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,72 @@
FROM python:3.12-slim
# Multi-stage build: gcc/libpq-dev and pip's build cache are only needed to
# *install* the dependencies below, not to run them (nothing here uses
# psycopg — asyncpg, the actual Postgres driver, ships its own compiled
# protocol implementation and needs neither). Keeping them confined to this
# "build" stage means they never end up in the final image, which the single-
# stage version of this Dockerfile used to ship unnecessarily.
FROM python:3.12-slim AS build

RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev gcc \
&& rm -rf /var/lib/apt/lists/*

# Installing into a venv (rather than the system site-packages) means the
# final stage can grab everything with one `COPY --from=build /opt/venv`,
# instead of having to know the exact site-packages path to copy.
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

WORKDIR /app

# CPU by default: pin the CPU-only torch wheel so the image doesn't pull
# CUDA deps it can't use (sentence-transformers, pulled in via pyproject.toml
# below, and surya-ocr, pulled in via document_processing/ocr/requirements.txt,
# both depend on torch). Paired with the "gpu" compose profile, build with
# --build-arg GPU=1 to skip the pin and let their default CUDA-enabled wheel
# install instead.
ARG GPU=0
RUN if [ "$GPU" = "0" ]; then \
pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu; \
fi

# app, field_mapping, translation, ocr, and gateway each keep their own
# dependency manifest, installed as-is — this image runs all five as
# separate uvicorn processes behind one entrypoint (scripts/start-combined.sh),
# it doesn't merge their code. Manifests are copied (and installed) before
# the rest of the source so this layer is only invalidated when a dependency
# changes, not on every code edit.
#
# The four requirements.txt files are staged in /deps rather than under their
# real paths on purpose: `pip install .` below relies on setuptools' flat-
# layout discovery in /app, which aborts if it finds sibling top-level package
# directories (field_mapping_poc/, document_processing/, gateway/) next to
# pyproject.toml. Keeping /app empty apart from pyproject.toml at install time
# preserves the behaviour this step had when it built app/ alone.
COPY pyproject.toml ./
COPY field_mapping_poc/requirements.txt /deps/field_mapping.txt
COPY document_processing/translation/requirements.txt /deps/translation.txt
COPY document_processing/ocr/requirements.txt /deps/ocr.txt
COPY gateway/requirements.txt /deps/gateway.txt

RUN pip install --no-cache-dir . \
&& pip install --no-cache-dir -r /deps/field_mapping.txt \
&& pip install --no-cache-dir -r /deps/translation.txt \
&& pip install --no-cache-dir -r /deps/ocr.txt \
&& pip install --no-cache-dir -r /deps/gateway.txt

# Final stage: no compiler, no dev headers, no pip build cache — just the
# Python runtime, the installed venv, and the source.
FROM python:3.12-slim

WORKDIR /app

COPY pyproject.toml .
RUN pip install --no-cache-dir .
COPY --from=build /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY . .

EXPOSE 8000
RUN chmod +x scripts/start-combined.sh

EXPOSE 8000 8001 8002 8010 8080

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["scripts/start-combined.sh"]
74 changes: 52 additions & 22 deletions lending-poc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,28 @@ see [Using a GPU](#using-a-gpu) below.
docker compose up --build -d
```

This builds and starts every service: `db`, `app`, `ollama`, `field_mapping`,
`translation`, `surya-inference` + `ocr`, `gateway`, and `frontend`.
This builds and starts every service: `db`, `ollama`, `surya-inference`,
`backend` (which runs `app`, `field_mapping`, `translation`, `ocr`, and
`gateway` as five processes in one container — see
[Inside the backend container](#inside-the-backend-container) below), and
`frontend`.

The first run takes a while — Ollama and Surya both download models on
first use. Watch progress with:
first use, and `backend`'s image build is the slowest of the bunch (it
installs every service's dependencies, including two separate CPU-only
PyTorch installs). Watch progress with:

```bash
docker compose logs -f
```

## 3. Run database migrations

The `app` container doesn't run migrations automatically on startup:
`app` (one of the five processes in the `backend` container) doesn't run
migrations automatically on startup:

```bash
docker compose exec app alembic -c db/alembic.ini upgrade head
docker compose exec backend alembic -c db/alembic.ini upgrade head
```

## 4. Pull the Ollama model
Expand All @@ -80,23 +86,45 @@ the command above.)
| Service | URL | Notes |
|---|---|---|
| Frontend | http://localhost:5173 | Main UI |
| Gateway | http://localhost:8080 | Fronts OCR / translation / field-mapping |
| App (backend API) | http://localhost:8000 | Docs at `/docs`; health at `/health` |
| Gateway | http://localhost:8080 | Single public entrypoint — fronts app/OCR/translation/field-mapping |
| App (cases API) | http://localhost:8000 | Docs at `/docs`; also reachable via gateway at `/cases`, `/app/health` |
| Postgres | localhost:55439 | pgvector-enabled |
| OCR | http://localhost:8010 | Not normally called directly |
| Translation | http://localhost:8001 | Not normally called directly |
| Field mapping | http://localhost:8002 | Not normally called directly |
| OCR | http://localhost:8010 | Also reachable via gateway at `/extract`, `/ocr/health` |
| Translation | http://localhost:8001 | Also reachable via gateway at `/translate/*`, `/translation/health` |
| Field mapping | http://localhost:8002 | Also reachable via gateway at `/map`, `/field-mapping/health` |
| Surya inference | http://localhost:8500 | OCR's inference backend |

There are effectively two subsystems sharing this compose file: the
`app` + `db` lending backend, and a separate OCR/translation/field-mapping
pipeline fronted by `gateway`. The frontend talks to the gateway for
document processing and to the app for everything else.
`app`, `ocr`, `translation`, `field_mapping`, and `gateway` all run inside
the single `backend` container (see below) — the four backend ports above
are published straight from that container for direct debugging, but the
frontend and any external caller should go through the gateway on `:8080`,
which proxies to all four and exposes one aggregate `/health`.

### Inside the backend container

`backend` (and its GPU twin `backend-gpu`) runs five independent
processes rather than one — `scripts/start-combined.sh` starts each with
its own `uvicorn` command, on its own port, exactly as it would run
standalone:

| Process | Port | Role |
|---|---|---|
| `app` | 8000 | Case submission and decisioning (`/cases`) |
| `translation` | 8001 | OCR-text translation |
| `field_mapping` | 8002 | Maps OCR text onto a target JSON schema |
| `ocr` | 8010 | Document text extraction (calls `surya-inference`) |
| `gateway` | 8080 | Reverse-proxies to the other four on one public port |

None of the five services' own code is aware they share a container —
each is the same FastAPI app it would be if it ran alone, just co-located
for fewer containers to manage. `docker compose logs -f backend` shows
all five processes' output interleaved, prefixed the same way regardless
of which one logged it.

## Using a GPU

`surya-inference`/`ocr` and `ollama` each come in a CPU and a GPU variant,
selected by `COMPOSE_PROFILES` in `.env`:
`surya-inference`, `backend` (which includes `ocr`), and `ollama` each
come in a CPU and a GPU variant, selected by `COMPOSE_PROFILES` in `.env`:

- `COMPOSE_PROFILES=cpu` (default) — always works, no GPU required.
- `COMPOSE_PROFILES=gpu` — requires an NVIDIA GPU on the host plus the
Expand All @@ -119,11 +147,13 @@ fail to create the containers at all if the toolkit isn't installed,
since the GPU device reservation can't be satisfied.

Ollama's own image auto-detects CUDA at runtime with no separate build, so
switching the profile is enough for it; `surya-inference`/`ocr` are built
from CUDA base images specifically for the `gpu` profile (see
[docker-compose.yml](docker-compose.yml) and
switching the profile is enough for it. `surya-inference` is built from a
CUDA base image for the `gpu` profile (see
[document_processing/ocr/README.md](document_processing/ocr/README.md)
for details).
for details); `backend-gpu` instead passes a `GPU=1` build arg that skips
pinning the CPU-only PyTorch wheel, so `ocr` (surya-ocr) and `app`
(sentence-transformers) install CUDA-enabled PyTorch instead — see
[docker-compose.yml](docker-compose.yml) and the [Dockerfile](Dockerfile).

## Stopping and cleanup

Expand All @@ -132,8 +162,8 @@ docker compose down
```

Add `-v` to also delete the named volumes (`pgdata`, `ollama_models`,
`surya_models`) — this wipes the database and downloaded models, so only
do this if you want a clean slate:
`surya_models`, `hf_cache`) — this wipes the database and downloaded
models, so only do this if you want a clean slate:

```bash
docker compose down -v
Expand Down
Loading