Skip to content
Merged
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
31 changes: 31 additions & 0 deletions credit/Dockerfile.serve.demo
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Credit CSS FastAPI — demo bake image (joblib inside the image, no volume).
# Build from repo root AFTER preparing the artifact:
# ./platform/scripts/prepare-serve-demo-artifact.sh
# docker build -f credit/Dockerfile.serve.demo -t credit-css-serve-demo .
# Run (no -v mount):
# docker run --rm -p 8000:8000 credit-css-serve-demo
#
# Local volume-mount serving stays on credit/Dockerfile.serve.

FROM python:3.11-slim-bookworm

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY pyproject.toml poetry.lock README.md ./
COPY src ./src
COPY credit ./credit

RUN pip install --no-cache-dir poetry \
&& poetry config virtualenvs.create false \
&& poetry install --only main --no-ansi --no-interaction

# Baked hermetic fit output (gitignored under credit/demo_artifact/).
COPY credit/demo_artifact/pipeline.joblib /artifact/pipeline.joblib
ENV CREDIT_PIPELINE_ARTIFACT=/artifact/pipeline.joblib
EXPOSE 8000

CMD ["uvicorn", "credit.api:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"]
17 changes: 9 additions & 8 deletions credit/docs/features/aws-fargate-serve-demo.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Feature: Ephemeral CSS FastAPI serve on AWS (Fargate)

**Status:** planned — docs lock in progress (#172); bake-image (#173) then AWS HITL (#174)
**Status:** in progress — docs done (#172); **bake-image (#173) in this cut**; AWS HITL (#174) next
**GitHub:** parent [#171](https://github.com/JLaborda/SmartWealthAI/issues/171) · [#172](https://github.com/JLaborda/SmartWealthAI/issues/172) docs · [#173](https://github.com/JLaborda/SmartWealthAI/issues/173) bake-image · [#174](https://github.com/JLaborda/SmartWealthAI/issues/174) AWS up/down
**Domain:** credit / CSS (demo surface) + shared `platform/` (IaC)
**Glossary:** [`../../CONTEXT.md`](../../CONTEXT.md) (domain only — no AWS terms) · Map: [`../../../CONTEXT-MAP.md`](../../../CONTEXT-MAP.md)
**Related:** chapter 5 [`css-chapter5-mart-and-scoring.md`](css-chapter5-mart-and-scoring.md) · PSI [`psi-data-drift.md`](psi-data-drift.md) · Friday MUST [`../guides/friday-demo.md`](../guides/friday-demo.md) · [ADR-0004](../../../docs/adr/0004-platform-ephemeral-fargate-serve.md)
**Code (target):** `platform/terraform/credit-css-serve-demo/`, `platform/scripts/demo-up.sh`, `platform/scripts/demo-down.sh`, `credit/Dockerfile.serve.demo`
**Related:** chapter 5 [`css-chapter5-mart-and-scoring.md`](css-chapter5-mart-and-scoring.md) · PSI [`psi-data-drift.md`](psi-data-drift.md) · Friday MUST [`../guides/friday-demo.md`](../guides/friday-demo.md) · [ADR-0004](../../../docs/adr/0004-platform-ephemeral-fargate-serve.md) · bake guide [`../guides/serve-fastapi.md`](../guides/serve-fastapi.md)
**Code:** `credit/demo_serve_artifact.py`, `credit/Dockerfile.serve.demo`, `platform/scripts/prepare-serve-demo-artifact.sh` · Tests: `tests/credit/test_serve_demo_bake.py` · Later: `platform/terraform/credit-css-serve-demo/`, `demo-up.sh` / `demo-down.sh`

## Problem Statement

Expand Down Expand Up @@ -66,11 +66,12 @@ No S3 data lake, no auth, no GitHub CD in this cut.

## Acceptance criteria

- [ ] Feature spec + ADR-0004 + friday-demo stretch link exist in English
- [ ] With a valid `AWS_PROFILE`, `demo-up` yields reachable `/health` and successful `/score` + `/drivers` using existing serving fixtures
- [ ] `demo-down` destroys the stack including ECR images
- [ ] Local volume-mount serve Docker path remains documented and unchanged in behaviour
- [ ] No AWS vocabulary added to `credit/CONTEXT.md`
- [x] Feature spec + ADR-0004 + friday-demo stretch link exist in English (#172)
- [x] Bake-image local path: `prepare-serve-demo-artifact` + `Dockerfile.serve.demo` + hermetic pytest + documented docker smoke without volume (#173)
- [ ] With a valid `AWS_PROFILE`, `demo-up` yields reachable `/health` and successful `/score` + `/drivers` using existing serving fixtures (#174)
- [ ] `demo-down` destroys the stack including ECR images (#174)
- [x] Local volume-mount serve Docker path remains documented and unchanged in behaviour
- [x] No AWS vocabulary added to `credit/CONTEXT.md`

## Out of Scope

Expand Down
16 changes: 14 additions & 2 deletions credit/docs/guides/serve-fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ curl -s localhost:8000/drivers \
-d @tests/credit/fixtures/serving/drivers_request.json
```

## Docker
## Docker (volume mount)

```bash
docker build -f credit/Dockerfile.serve -t credit-css-serve .
Expand All @@ -68,6 +68,18 @@ docker run --rm -p 8000:8000 \
credit-css-serve
```

## Docker (bake-image demo — no volume)

Hermetic fit into gitignored `credit/demo_artifact/`, then an image that embeds `pipeline.joblib` (for local smoke and later Fargate). Spec: [`../features/aws-fargate-serve-demo.md`](../features/aws-fargate-serve-demo.md) (#173).

```bash
./platform/scripts/prepare-serve-demo-artifact.sh
docker build -f credit/Dockerfile.serve.demo -t credit-css-serve-demo .
docker run --rm -p 8000:8000 credit-css-serve-demo
```

Same curls as above (no `-v`). Volume-mount `Dockerfile.serve` remains the default local path.

## Out of this guide

PSI CLI, Terraform/AWS, SHAP on drivers — later Friday-demo slices.
PSI CLI, Terraform/AWS up-down (#174), SHAP on drivers — later slices.
54 changes: 54 additions & 0 deletions credit/src/credit/demo_serve_artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Prepare a hermetic ``pipeline.joblib`` for the bake-image serve demo (#173)."""

from __future__ import annotations

from pathlib import Path

from credit.application_mart import build_application_mart
from credit.scoring_pipeline import ARTIFACT_NAME, fit_scoring_pipeline

# Default relative to repo root when callers pass only output_dir via the script.
DEFAULT_SCORING_FIXTURE = (
Path(__file__).resolve().parents[3]
/ "tests"
/ "credit"
/ "fixtures"
/ "scoring_mart"
/ "applications.csv"
)


def prepare_serve_demo_artifact(
output_dir: Path,
*,
source: Path | None = None,
) -> Path:
"""Fit a CSS pipeline into ``output_dir`` and return the joblib path.

Uses the hermetic scoring fixture unless ``source`` is provided. Mart
intermediates go under ``output_dir / "_mart"``.
"""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
source_path = Path(source) if source is not None else DEFAULT_SCORING_FIXTURE

mart = build_application_mart(
source_path,
output_dir / "_mart",
application_id_column="customer_ID",
target_column="target",
bad_value=1,
good_value=0,
)
fit_scoring_pipeline(
mart.mart_path,
output_dir,
application_id_column="customer_ID",
target_column="target",
random_state=42,
skip_mlflow=True,
)
artifact = output_dir / ARTIFACT_NAME
if not artifact.is_file():
raise FileNotFoundError(f"Expected artifact missing: {artifact}")
return artifact
11 changes: 11 additions & 0 deletions platform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Platform

Shared AWS / Terraform and operator scripts. Domain language stays in `credit/` and investing docs — see [`CONTEXT-MAP.md`](../CONTEXT-MAP.md) and [ADR-0004](../docs/adr/0004-platform-ephemeral-fargate-serve.md).

## Scripts (credit CSS serve demo)

| Script | Purpose |
| --- | --- |
| `scripts/prepare-serve-demo-artifact.sh` | Hermetic fit → `credit/demo_artifact/pipeline.joblib` for `credit/Dockerfile.serve.demo` (#173) |

AWS `demo-up` / `demo-down` land with [#174](https://github.com/JLaborda/SmartWealthAI/issues/174).
15 changes: 15 additions & 0 deletions platform/scripts/prepare-serve-demo-artifact.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Hermetic fit → credit/demo_artifact/pipeline.joblib for Dockerfile.serve.demo (#173).
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"

OUT="${CREDIT_DEMO_ARTIFACT_DIR:-$ROOT/credit/demo_artifact}"

poetry run python -c "
from pathlib import Path
from credit.demo_serve_artifact import prepare_serve_demo_artifact
path = prepare_serve_demo_artifact(Path(r'''$OUT'''))
print(path)
"
75 changes: 75 additions & 0 deletions tests/credit/test_serve_demo_bake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Seam A: prepare demo artifact → FastAPI score/drivers (bake path, no Docker)."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from credit.scoring_pipeline import ARTIFACT_NAME

EXAMPLE_PAYLOAD = Path(__file__).parent / "fixtures" / "serving" / "score_request.json"
DRIVERS_PAYLOAD = Path(__file__).parent / "fixtures" / "serving" / "drivers_request.json"
SCORING_FIXTURE = Path(__file__).parent / "fixtures" / "scoring_mart" / "applications.csv"


def test_prepare_serve_demo_artifact_serves_score_and_drivers(tmp_path: Path) -> None:
"""Hermetic prepare → joblib under output_dir → /score and /drivers succeed."""
from credit.api import create_app
from credit.demo_serve_artifact import prepare_serve_demo_artifact

artifact = prepare_serve_demo_artifact(
tmp_path / "demo_artifact",
source=SCORING_FIXTURE,
)
assert artifact == tmp_path / "demo_artifact" / ARTIFACT_NAME
assert artifact.is_file()

client = TestClient(create_app(artifact_path=artifact))
score_body = json.loads(EXAMPLE_PAYLOAD.read_text(encoding="utf-8"))
drivers_body = json.loads(DRIVERS_PAYLOAD.read_text(encoding="utf-8"))

score = client.post("/score", json=score_body)
assert score.status_code == 200, score.text
score_payload = score.json()
assert score_payload["application_id"] == score_body["application_id"]
assert 0.0 <= score_payload["pd"] <= 1.0
assert 250.0 <= score_payload["credit_score"] <= 1000.0

drivers = client.post("/drivers", json=drivers_body)
assert drivers.status_code == 200, drivers.text
drivers_payload = drivers.json()
assert drivers_payload["method"] == "gain_x_abs_woe"
assert len(drivers_payload["drivers"]) == drivers_body["top_k"]


def test_prepare_uses_default_scoring_fixture_when_source_omitted(
tmp_path: Path,
) -> None:
"""Operator script path: no ``source`` → DEFAULT_SCORING_FIXTURE."""
from credit.demo_serve_artifact import (
DEFAULT_SCORING_FIXTURE,
prepare_serve_demo_artifact,
)

assert DEFAULT_SCORING_FIXTURE.is_file()
artifact = prepare_serve_demo_artifact(tmp_path / "demo_default")
assert artifact == tmp_path / "demo_default" / ARTIFACT_NAME
assert artifact.is_file()


def test_prepare_raises_when_fit_skips_writing_joblib(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Fail clearly if fit returns without ``pipeline.joblib``."""
from credit import demo_serve_artifact as mod

monkeypatch.setattr(
mod,
"fit_scoring_pipeline",
lambda *a, **k: None,
)
with pytest.raises(FileNotFoundError, match="Expected artifact missing"):
mod.prepare_serve_demo_artifact(tmp_path / "empty", source=SCORING_FIXTURE)
Loading