Skip to content
Closed
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
165 changes: 145 additions & 20 deletions INTERVIEW.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,153 @@

## 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, 3, 4, 5 and 6 each have a failing test that tells you what to fix. Tasks 2 and 7 are verified by running the app. You can refer to Google or relevant documentation — no AI-assisted coding tools.

---

### Running the tests

**Every task has its own command** — `make test-1` through `make test-7`. Run only the one for the task you are on:

```
make test-4
```

`make test` runs everything at once. Avoid it until the end: tests for tasks you have not reached yet will fail and bury the output you care about.

**Read the last few lines of the output.** Each test failure ends with the assertion message that names exactly what is wrong, for example:

```
E AssertionError: Incorrect number of interests
```

That message is the whole signal — you do not need to read the traceback above it.

**The tests are correct.** Nearly every fix belongs in the source code, in `api/` or `model/` — not in `tests/`. **Task 4 is the only exception:** `top_k` has to be passed into the request as a query parameter, so that test does change.

---

### 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-1`. The test fails.

Find and fix the bug in the model code, then verify:
```
make test-1
```

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 start the server again and leave it running in its terminal:
```
make serve-api
```

From a second terminal, verify:
```
make test-2
```

---

**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-3
```

This test exercises the model in memory. For the **served** model to return probabilities too, re-run `make train` and restart `make serve-model`.

---

**4. Fix the interests count**

With both servers running, run `make test-4`. It fails with:
```
AssertionError: Incorrect number of interests
```

The test asks for 15 interests but never tells the API how many it wants. Make the request send `top_k`, and make sure the API honours it. Verify:
```
make test-4
```

This is the one task where you edit a test.

---

**5. Fix response time**

With both servers running, run `make test-5`. It fails with:
```
AssertionError: Request greater than 1 second
```

Find the performance bottleneck and fix it. Verify:
```
make test-5
```

---

**6. Add probability to the response**

With both servers running, run `make test-6`. It fails with:
```
AssertionError: Interest object missing 'probability': ...
```

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-6
```

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 — check the response yourself:
```
make test-7
```
56 changes: 54 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,66 @@
PYTEST = TF_CPP_MIN_LOG_LEVEL=3 pytest
HANDLE ?= e337a675-46f5-437e-aa72-5d43643b5461

.PHONY: help setup serve-api serve-model ping-model train test \
test-1 test-2 test-3 test-4 test-5 test-6 test-7

help:
@printf 'Setup and servers\n'
@printf ' make setup Install dependencies\n'
@printf ' make train Train the model and export it to serving/\n'
@printf ' make serve-model Start TF Serving on port 8501\n'
@printf ' make serve-api Start the Flask API on port 5005\n'
@printf ' make ping-model Check that TF Serving has loaded the model\n'
@printf '\nInterview tasks (run only the target for the task you are on)\n'
@printf ' make test-1 Task 1: model build\n'
@printf ' make test-2 Task 2: API starts up\n'
@printf ' make test-3 Task 3: probability scores\n'
@printf ' make test-4 Task 4: interests count\n'
@printf ' make test-5 Task 5: response time\n'
@printf ' make test-6 Task 6: probability in the response\n'
@printf ' make test-7 Task 7: min_probability filtering\n'
@printf ' make test Every test at once (expect noise until all tasks are done)\n'

setup:
pip install -r requirements.txt

serve-api:
python -m flask --app api/app run --debug --port 5005 --host 0.0.0.0

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

test:
TF_CPP_MIN_LOG_LEVEL=3 pytest ./tests -p no:warnings
$(PYTEST)

test-1:
$(PYTEST) tests/test_model.py::TestModel::test_model_build

# No automated test — the API only starts once the bug is fixed.
test-2:
@curl -fs http://localhost:5005/ \
&& printf '\nPASS: the API is up\n' \
|| printf 'FAIL: the API is not reachable on port 5005 (see task 2)\n'

test-3:
$(PYTEST) tests/test_model.py::TestModel::test_probability_scores

test-4:
$(PYTEST) tests/test_app.py::TestInterestsAPI::test_interests_count

test-5:
$(PYTEST) tests/test_app.py::TestInterestsAPI::test_response_time

test-6:
$(PYTEST) tests/test_app.py::TestInterestsAPI::test_with_probability_threshold

# No automated test — read the response and check every probability is >= 0.5.
test-7:
@curl -fsS "http://localhost:5005/interests/$(HANDLE)?min_probability=0.5" \
| python3 -m json.tool
Loading