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
9 changes: 9 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM mcr.microsoft.com/devcontainers/python:3.10

COPY zscaler-bundle.pem /tmp/zscaler.pem
RUN if [ -s /tmp/zscaler.pem ]; then \
cp /tmp/zscaler.pem /usr/local/share/ca-certificates/zscaler.crt && \
update-ca-certificates; \
fi && rm -f /tmp/zscaler.pem

RUN find /etc/apt /usr/share/keyrings -name "*yarn*" -delete 2>/dev/null || true
9 changes: 9 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:3": {
"version": "3.0.1",
"resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:ca2508495b01ba29eba93e8153772a2daa65eaa86471cc6863fe2a3d21933df9",
"integrity": "sha256:ca2508495b01ba29eba93e8153772a2daa65eaa86471cc6863fe2a3d21933df9"
}
}
}
13 changes: 8 additions & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "MLE Interview",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:1-3.10",
"build": { "dockerfile": "Dockerfile" },
"initializeCommand": "cp ~/.ssl/zscaler-bundle.pem .devcontainer/zscaler-bundle.pem 2>/dev/null || touch .devcontainer/zscaler-bundle.pem",
"customizations": {
"vscode": {
"settings": {
Expand All @@ -14,7 +14,7 @@
"source.organizeImports.ruff": true,
"source.organizeImports": true
}
},
}
},
"extensions": [
"charliermarsh.ruff",
Expand All @@ -29,7 +29,10 @@
}
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker": {}
"ghcr.io/devcontainers/features/docker-in-docker:3": {
"moby": false,
"disableIp6tables": true
}
},
"postCreateCommand": "make setup"
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Zscaler cert (machine-specific, copied by initializeCommand)
.devcontainer/zscaler-bundle.pem

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
133 changes: 113 additions & 20 deletions INTERVIEW.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,121 @@

## Coding Challenge

You can refer to Google or relevant documentation to guide you in addressing the issues below.
You are not allowed to use ChatGPT, Copilot, or any similar tools for assistance.
You are working on a user interest recommendation system. It consists of:

- A **TensorFlow model** trained on user content interactions, served via TF Serving (Docker)
- A **Flask API** that fetches user features, calls the model, and returns ranked interests
- A **feature store** backed by CSV files

There are **7 tasks** to complete. Tasks 1–6 each have a failing test or a clear broken behaviour that tells you what to fix. Task 7 is an open-ended feature implementation. You can refer to Google or relevant documentation — no AI-assisted coding tools.

---

### Terminal setup (do this before task 4)

Once the model is trained and served, you'll need **two terminals** running simultaneously inside the container:

| Terminal | Command | Purpose |
|---|---|---|
| 1 | `make serve-model` | TF Serving on port 8501 |
| 2 | `make serve-api` | Flask API on port 5005 |

Keep both running while working on tasks 4–7.

---

### Tasks

**1. Fix model training**

Run `make test`. The `TestModel::test_model_build` test fails.

Find and fix the bug in the model code, then verify:
```
make test # TestModel::test_model_build must pass
```

Once passing, train the model and start the model server (leave it running):
```
make train
make serve-model
```

---

**2. Fix the API startup**

Run `make serve-api`. The Flask application fails to start.

Find and fix the bug, then verify the server starts without errors:
```
make serve-api
```

Leave the server running in its terminal.

---

**3. Fix the probability scores**

The model's serving function returns raw logit scores, not probabilities. These values are not bounded between 0 and 1 and cannot be meaningfully compared or filtered.

Find where the issue originates and fix it so that the scores returned represent proper probability values. There are two valid approaches — both are acceptable.

Verify:
```
make test # TestModel::test_probability_scores must pass
```

---

**4. Fix the interests count**

With both servers running, run `make test`. This test fails:
```
TestInterestsAPI::test_basic_response — AssertionError: Incorrect number of interests
```

Fix the API so it returns the correct number of interests. Verify:
```
make test # test_basic_response must pass (interests count assertion)
```

---

**5. Fix response time**

With both servers running, run `make test`. This test fails:
```
TestInterestsAPI::test_basic_response — AssertionError: Request greater than 1 second
```

Find the performance bottleneck and fix it. Verify:
```
make test # test_basic_response must pass (timing assertion)
```

---

**6. Add probability to the response**

With both servers running, run `make test`. This test fails:
```
TestInterestsAPI::test_with_probability_threshold
```

1. **Model Training Error Resolution:**
- Begin by executing `make test` to identify any issues during model training.
- After passing the `TestModel::test_model_build` test, use `make train` followed by `make serve-model` to train and serve the model, preparing it for upcoming steps.
Update the API response so each interest includes a `probability` field. Verify:
```
make test # test_with_probability_threshold must pass
```

2. **API Startup Issue Fix:**
- Resolve the Flask API startup issue with `make serve-api`.
- Run `make serve-api` to start a development Flask server, then open a new terminal window without shutting down the server.
---

3. **Data Return and Interests Length Correction:**
- Execute `make test` and resolve the `TestInterestsAPI::test_basic_response - AssertionError: Incorrect number of interests` test.
- Ensure the return data structure and interests array length meet the specifications, adjusting the API as necessary.
**7. Add probability threshold filtering**

4. **Response Time Optimization:**
- Run `make test` and fix the `TestInterestsAPI::test_basic_response - AssertionError: Request greater than 1 second` test.
- Enhance the API to achieve a response time of one second or less.
Implement a feature that lets API clients filter results by a minimum probability score. For example:

5. **Probability Field Inclusion:**
- Perform `make test` and correct the `TestInterestsAPI::test_with_probability` test.
- Update the API response to incorporate a 'probability' field.
```
GET /interests/<user_handle>?min_probability=0.5
```

6. **Probability Score Filtering Implementation:**
- Create a feature allowing API clients to filter results by probability score, enabling users to specify a probability threshold for more targeted results.
Should return only interests with probability ≥ 0.5. There is no automated test for this task — demonstrate it works with a curl request.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ serve-api:
serve-model:
docker compose up tf-serving

ping-model:
curl -s http://localhost:8501/v1/models/interests-model | python3 -m json.tool

train:
TF_CPP_MIN_LOG_LEVEL=3 python -m model.main

Expand Down
116 changes: 44 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,120 +1,92 @@
# MLE Interview:

# MLE Interview

## Setup
1. Install [Visual Studio Code](https://code.visualstudio.com/). Also the following plugins:
- [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. Clone this repo
3. Open the repository in VSCode (`code .`)
4. VSCode will prompt you to install recommended extensions. Select `Yes`
- If you missed this step, you can install recommended extensions using the extensions sidebar (`⇧⌘X`)
5. VSCode will prompt you to reopen the workspace in a Dev Container. Select `Yes`.
- If you missed this step, open the command prompt palette (`⇧⌘P`) and run the command `Remote-Containers: Rebuild and Reopen in Container`

### Option A — GitHub Codespaces (recommended)

Click **Code → Open with Codespaces** on the repository page. The environment builds automatically — no local installation needed.

### Option B — Local Dev Container (VS Code)

1. Install [Visual Studio Code](https://code.visualstudio.com/) and the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension
2. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
3. Clone this repo and open it in VS Code (`code .`)
4. VS Code will prompt you to reopen in a Dev Container — select **Yes**
- If you miss the prompt: `⇧⌘P` → **Dev Containers: Rebuild and Reopen in Container**

> **Note (corporate networks / Zscaler):** If your machine uses Zscaler SSL inspection, ensure your Zscaler certificate bundle exists at `~/.ssl/zscaler-bundle.pem` before building the container. The build handles this automatically.

Once the container starts, dependencies are installed automatically via `make setup`. You do not need to run it manually.


## Make Commands

Before you start, ensure you have all the necessary dependencies installed:
### Test

```
make setup
make test
```

This command will install the Python dependencies listed in `requirements.txt`.
Runs all tests in `./tests` with pytest. Use this to verify each task as you work through the interview.

### Serve API

To start the Flask API, use the following command:
### Train

```
make serve-api
make train
```

This command launches the Flask application defined in `api/app` on port 5005, accessible via `0.0.0.0`. The `--debug` flag is included to enable debug mode, which provides useful debugging information in case of errors and automatically reloads the server on code changes.
Trains the model using `model/main.py` and exports a SavedModel to `serving/interests-model/1/`.

### Serve Model

If you're using a TensorFlow model served via Docker, you can start the TensorFlow serving using:

```
make serve-model
```

This command uses Docker Compose to spin up a container defined in the Docker configuration, specifically targeting the TensorFlow serving service (`tf-serving`). Ensure Docker is installed and running on your system before executing this command.
Starts TF Serving via Docker Compose on port 8501. Run this after `make train` — the model must be trained first.

### Train

To train the model, execute:
### Serve API

```
make train
make serve-api
```

This command runs the main training script located at `model.main`.
Starts the Flask development server on port 5005. Requires the model server to be running for inference endpoints to work.

### Test

Finally, to run the tests for your project, use:
## API

```
make test
GET http://localhost:5005/interests/<user_handle>
```

This command runs all the tests in the `./tests` directory using pytest.
Optional query parameters:
- `top_k` (int) — number of interests to return (default: 10)
- `min_probability` (float) — filter results below this probability score

Example response:

## API

Route: GET: http://localhost:5005/interests/:userHandle

API Response:

```
```json
{
"user_handle": "e337a675-46f5-437e-aa72-5d43643b5461",
"name": "Kisiza",
"type": "B2B",
"interests": [
{
"id": "29e020bb-7a02-41db-b21f-527a8ef4dfdf",
"label": "Accounting technician"
"label": "Accounting technician",
"probability": 0.94
},
{
"id": "012abcd1-3a6a-4803-a47e-42f46b402024",
"label": "Field seismologist"
"label": "Field seismologist",
"probability": 0.87
},
{
"id": "a1b028dd-8464-4c63-85e8-ae29ea184fc7",
"label": "Designer, industrial/product"
},
{
"id": "686af7e4-6d58-4148-b227-3bf65ff10273",
"label": "Materials engineer"
},
{
"id": "8d1526b9-a7bf-4972-be43-7b912f149667",
"label": "Fashion designer"
},
{
"id": "d83a55a3-0143-4318-91c1-88f44ad59390",
"label": "Journalist, newspaper"
},
{
"id": "cda4441f-dba6-495c-9e2e-7429bd5e0465",
"label": "Therapist, music"
},
{
"id": "e9b23ad4-753b-4331-9cfa-525965fcf281",
"label": "Secretary, company"
},
{
"id": "ca4b03b2-0ae2-440a-afc8-5469510b19cb",
"label": "Commissioning editor"
},
{
"id": "fd3c41b8-8c15-47e2-a80d-cf3683b2d0da",
"label": "Copywriter, advertising"
"label": "Designer, industrial/product",
"probability": 0.81
}
],
"name": "Kisiza",
"type": "B2B"
]
}
```
```
Loading