Serving
The serving module is the app contract half of the two-contracts architecture (ADR-0006). It exposes a small, OpenAPI-documented HTTP API on top of the data platform and ranker so application clients — the TypeScript editor, the Streamlit swap-demo, anything else that speaks the contract — can drive the recommender without touching DuckDB directly.
The whole module is also where most of the FastAPI + Python code lives that
the other modules anchor to. If you are exploring the repo top-down,
tutorial/serving/
is the directory with the most going on.
The runnable rep
Section titled “The runnable rep”Rep #6 makes the contract’s heart concrete without a server. Every
/recommendations and /preview response is two pure functions stacked:
recommend_for_user(...) produces a candidate set (the model), then
rank_candidates(...) turns it into a ranked list under an editor’s policy.
tutorial/serving/demo_recommendations.py
runs that exact stack on a tiny in-memory corpus.
make -C tutorial/serving run # candidate -> rank flow, two policies, no servermake -C tutorial/serving check # the full serving suite (162 tests)make -C tutorial/serving clean # remove pytest cachesThe demo gives a reader whose history is all sport, so the candidate set leans sport, then ranks it two ways:
- click-only baseline (all weights 0) →
s3 s1 s2 c1 p1— a pure-relevance sport block. - diversity-forward (diversity_weight 0.8) →
s3 c1 s1 p1 s2.
The checkpoint, worked
Section titled “The checkpoint, worked”Which non-sport article does the diversity-forward policy pull up, and why is that an editorial decision, not a model decision?
c1 (culture) jumps from position 4 to position 2. The model ranked all
three sport articles above it — the reader only reads sport, so by relevance
that is correct. Pulling culture up is the newsroom overriding “what this
reader will click” with “what this reader should be offered.” That is the
editorial cost of clicks made tunable: the same candidate set, two policies,
two different fronts — and the model never changed.
The endpoints
Section titled “The endpoints”| Endpoint | What it does | Source |
|---|---|---|
GET /articles/{id} | Single-article lookup | articles.py |
GET /recommendations/{user_id} | Generates a candidate set, applies the persisted configuration’s ranker, returns the ranked list | recommendations.py |
POST /preview | Same ranker, but takes the configuration inline (does not persist). Powers the editor’s live preview. | recommendations.py |
GET / POST / PUT / DELETE /constraint-configurations* | Full CRUD against the constraint_configurations table | configurations.py |
POST /assistants/change-explainer | Editorial AI assistant: explains why a recommendation list changed | agents/change-explainer.ts (TS side) + serving glue |
The application entry point is
app.py.
The canonical app-contract schema source is the checked-in OpenAPI YAML catalog
at
editor/openapi/app-contract-sources.json.
The API reference and TypeScript editor clients are
generated from that catalog. FastAPI still serves its own /openapi.json, but
the drift gate compares that served schema with the catalog entries marked as
FastAPI-owned.
The OpenAPI catalog is the swappable boundary
Section titled “The OpenAPI catalog is the swappable boundary”The architectural claim (ADR-0006)
is that any client speaking the same OpenAPI contract is interchangeable
with any other. The TypeScript editor is one client; the Streamlit
swap-demo is a deliberately different one (different language, different
UI runtime) — and the
swap_demo.py
script makes the swappability claim falsifiable rather than rhetorical.
A side-by-side integration test feeds the same fake app-contract server to both clients and asserts they produce identical rankings for the same user and configuration. Read serving’s lesson prose for the full walkthrough.
Tests as documentation
Section titled “Tests as documentation”The
tests/
directory carries integration tests for every endpoint:
Each test makes the response schema explicit and verifies behaviour against the ranker module without going through the network. The OpenAPI contract is enforced at the boundary.
After this module
Section titled “After this module”The editor module is the primary application client. The assistants route is editor-owned but published through the same app-contract catalog. The evaluation module reads the same ranker behaviour the serving endpoints expose.