Skip to content

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.

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.

Terminal window
make -C tutorial/serving run # candidate -> rank flow, two policies, no server
make -C tutorial/serving check # the full serving suite (162 tests)
make -C tutorial/serving clean # remove pytest caches

The 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.

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.

EndpointWhat it doesSource
GET /articles/{id}Single-article lookuparticles.py
GET /recommendations/{user_id}Generates a candidate set, applies the persisted configuration’s ranker, returns the ranked listrecommendations.py
POST /previewSame 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 tableconfigurations.py
POST /assistants/change-explainerEditorial AI assistant: explains why a recommendation list changedagents/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.

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.

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.