From c7c178fe0217495912a94d5039edd838b643e130 Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:27:49 -0700 Subject: [PATCH 1/5] Add Dockerfile and document how to run Sentinel in a container Adds a Dockerfile (+ .dockerignore) that installs the sentinel library with the sbert extra and runs the beginner demo by default, plus a "Run with Docker" README section covering build, run, model caching, and custom scripts. Co-authored-by: Cursor --- .dockerignore | 24 ++++++++++++ Dockerfile | 52 ++++++++++++++++++++++++++ README.md | 58 +++++++++++++++++++++++++++++ examples/beginner_demo.py | 77 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 examples/beginner_demo.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..93ed738 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +# Keep the Docker build context small and avoid copying host-only artifacts. +.git +.github +.venv +.vscode +docs/_build + +# Python caches / build outputs +**/__pycache__ +*.py[cod] +*.egg-info +.pytest_cache +.mypy_cache +.ipynb_checkpoints + +# OS / editor cruft +.DS_Store +*~ +*.swp +*.swo + +# The Docker files themselves +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c662351 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# Sentinel container image. +# +# Builds a self-contained image that installs the `sentinel` library (with the +# optional `sbert` extra, which pulls in sentence-transformers + torch) and, by +# default, runs the beginner demo so you can see the full scoring flow. +FROM python:3.11-slim + +# - PYTHONUNBUFFERED: print logs immediately instead of buffering them. +# - PYTHONDONTWRITEBYTECODE: don't litter the image with .pyc files. +# - POETRY_VIRTUALENVS_CREATE=false: install deps into the system Python so we +# don't nest a virtualenv inside the container. +# - HF_HOME: where Hugging Face / sentence-transformers cache downloaded models. +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 \ + HF_HOME=/home/sentinel/.cache/huggingface + +# build-essential is needed to compile some scientific Python wheels. +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/* + +# The project's build backend requires poetry-core >= 2.0, so use Poetry 2.x. +RUN pip install "poetry>=2.0,<3.0" + +WORKDIR /app + +# Copy only what's needed to install dependencies first. Keeping this separate +# from the rest of the source lets Docker cache the (slow) dependency install +# and skip it when only application code changes. +COPY pyproject.toml poetry.lock README.md ./ +COPY src ./src + +# Install the library plus the `sbert` extra. Skip the dev/docs/examples groups +# to keep the image lean. +RUN poetry install --extras sbert --without dev,docs,examples + +# Copy the remaining source (examples, tests, docs, etc.). +COPY . . + +# Run as a non-root user for safety. +RUN useradd --create-home sentinel \ + && mkdir -p "$HF_HOME" \ + && chown -R sentinel:sentinel /app /home/sentinel +USER sentinel + +# By default, run the beginner demo end-to-end. Override this at `docker run` +# time to run your own script, e.g.: +# docker run --rm sentinel python examples/Example_Threshold_Script.py +CMD ["python", "examples/beginner_demo.py"] diff --git a/README.md b/README.md index b14d338..1febec6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,64 @@ To pull them in as well, use: pip install '.[sbert]' ``` +## Run with Docker + +If you'd rather not set up Python and Poetry locally, you can run Sentinel in a +container. A container is just an isolated, pre-configured "box" that already +has the right Python version and all dependencies installed, so it runs the +same way on any machine that has [Docker](https://docs.docker.com/get-docker/). + +### 1. Build the image + +From the repository root (the folder containing the `Dockerfile`): + +```bash +docker build -t sentinel . +``` + +This installs the `sentinel` library together with the `sbert` extra +(sentence-transformers + torch). The first build downloads a lot of +dependencies, so it can take several minutes. + +### 2. Run the demo + +```bash +docker run --rm sentinel +``` + +This runs `examples/beginner_demo.py`, which builds a tiny in-memory index and +scores a batch of example messages. On the first run it downloads the +`all-MiniLM-L6-v2` embedding model, so give it a moment. + +To avoid re-downloading that model on every run, mount a local folder as the +model cache: + +```bash +docker run --rm -v "$(pwd)/.hf-cache:/home/sentinel/.cache/huggingface" sentinel +``` + +### 3. Run your own script + +The default command is just a starting point. Override it to run any other +script in the image — for example the threshold tuning script: + +```bash +docker run --rm sentinel python examples/Example_Threshold_Script.py +``` + +Or drop into an interactive shell to explore: + +```bash +docker run --rm -it sentinel bash +``` + +To run a script from your own machine (not baked into the image), mount your +current directory into the container's `/app` folder: + +```bash +docker run --rm -v "$(pwd):/app" sentinel python your_script.py +``` + ## Quick Start ```python diff --git a/examples/beginner_demo.py b/examples/beginner_demo.py new file mode 100644 index 0000000..a1c8a93 --- /dev/null +++ b/examples/beginner_demo.py @@ -0,0 +1,77 @@ +""" +Beginner demo for Sentinel. + +Goal: see the whole Sentinel flow end-to-end in ~1 screen of code, with no +dataset downloads and no S3. We build a tiny index in memory, then score a +batch of messages. + +Run it with the project's venv active: + cd ~/workspace/Sentinel + source .venv/bin/activate + python examples/beginner_demo.py +""" + +import torch + +from sentinel.sentinel_local_index import SentinelLocalIndex +from sentinel.embeddings.sbert import get_sentence_transformer_and_scaling_fn + + +# 1) Load the "text -> numbers" model (downloads once, then cached locally). +model_name = "all-MiniLM-L6-v2" +model, scale_fn = get_sentence_transformer_and_scaling_fn(model_name) + +# 2) Define our two piles of examples. +# Positive = the rare class we want to catch (here: aggressive/threatening). +# Negative = normal, everyday chat. +positive_examples = [ + "I'm going to hurt you if you don't listen", + "You should be scared of what I'll do", + "I know where you live and I'm coming for you", + "Give me your password or you'll regret it", +] +negative_examples = [ + "Want to play a game later?", + "That movie was really fun, we should watch it again", + "Happy birthday! Hope you have a great day", + "Can you help me with my math homework?", + "The weather is nice today, let's go outside", +] + +# 3) Turn the example text into embeddings (lists of numbers capturing meaning). +positive_embeddings = torch.tensor( + model.encode(positive_examples, normalize_embeddings=True) +) +negative_embeddings = torch.tensor( + model.encode(negative_examples, normalize_embeddings=True) +) + +# 4) Build the index (this is the object that does the scoring). +index = SentinelLocalIndex( + sentence_model=model, + positive_embeddings=positive_embeddings, + negative_embeddings=negative_embeddings, + scale_fn=scale_fn, + positive_corpus=positive_examples, # keep the text so explanations are readable + negative_corpus=negative_examples, +) + +# 5) Score a batch of NEW messages (imagine: one user's recent chat). +new_messages = [ + "Hey how are you?", + "Do you want to grab lunch?", + "I'm going to make you pay for this", + "Cool, see you at 3pm", + "You better watch your back", +] + +result = index.calculate_rare_class_affinity(new_messages) + +# 6) Read the results. +print(f"\nOverall rare-class affinity score: {result.rare_class_affinity_score:.4f}") +print(f"Aggregator used: {result.aggregation_name}\n") + +print("Per-message scores (higher = more suspicious):") +for message, score in result.observation_scores.items(): + risk = "HIGH" if score > 0.5 else "MED" if score > 0.1 else "low" + print(f" [{risk:>4}] {score:.4f} {message}") From 4d3997c590959486c4d7c97317f5a1e862d6c23f Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:05:21 -0700 Subject: [PATCH 2/5] Fix broken code fences in README Close the unterminated Python block in the "Creating a New Index" section and remove a stray closing fence after the aggregation "Notes", which together caused headings and prose (through "How It Works") to render as code. Co-authored-by: Cursor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1febec6..8cfe2ec 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ saved_config = index.save( aws_access_key_id="YOUR_ACCESS_KEY_ID", # Optional if using environment credentials aws_secret_access_key="YOUR_SECRET_ACCESS_KEY" # Optional if using environment credentials ) +``` ## Testing for optimal Thresholds and data ratio's @@ -224,7 +225,6 @@ res4 = index.calculate_rare_class_affinity(texts, aggregation_function=max_score Notes: - All aggregators operate over per‑observation scores where non‑confident observations are already clipped to 0. - The default `skewness` remains a good choice when user activity volume varies widely. -``` ## How It Works From c8c6606b28b060b762dd3af3bd0504e3baa4a3ca Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:14:08 -0700 Subject: [PATCH 3/5] Add CPU-only Docker image variant Add Dockerfile.cpu that installs the CPU-only torch build (from the PyTorch CPU index) so users who don't need a GPU can build a much smaller image (~1.5-2.5 GB vs ~6-8 GB). Cross-reference it from the default Dockerfile and document both variants (with a comparison table) in the README. Co-authored-by: Cursor --- Dockerfile | 7 ++++++- Dockerfile.cpu | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 22 +++++++++++++++++-- 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 Dockerfile.cpu diff --git a/Dockerfile b/Dockerfile index c662351..a7d7a02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,13 @@ -# Sentinel container image. +# Sentinel container image (default / GPU-capable variant). # # Builds a self-contained image that installs the `sentinel` library (with the # optional `sbert` extra, which pulls in sentence-transformers + torch) and, by # default, runs the beginner demo so you can see the full scoring flow. +# +# This variant installs the default torch build, which on Linux is GPU (CUDA) +# enabled and therefore large (~6-8 GB total image). Sentinel itself runs on +# CPU; if you don't need GPU, use `Dockerfile.cpu` for a much smaller image: +# docker build -f Dockerfile.cpu -t sentinel:cpu . FROM python:3.11-slim # - PYTHONUNBUFFERED: print logs immediately instead of buffering them. diff --git a/Dockerfile.cpu b/Dockerfile.cpu new file mode 100644 index 0000000..e2408d3 --- /dev/null +++ b/Dockerfile.cpu @@ -0,0 +1,57 @@ +# Sentinel container image (CPU-only variant). +# +# Same as the default `Dockerfile`, but installs the CPU-only build of torch +# instead of the default CUDA/GPU build. Sentinel's code does not use a GPU, so +# for most deployments this is all you need -- and it produces a MUCH smaller +# image (the default GPU build pulls in ~5-6 GB of NVIDIA CUDA libraries that +# never get used on CPU). +# +# Build it explicitly with the -f flag: +# docker build -f Dockerfile.cpu -t sentinel:cpu . +# +# Note: unlike the default Dockerfile (which uses Poetry + poetry.lock for a +# fully pinned install), this variant installs with pip so it can pull torch +# from the dedicated PyTorch CPU package index. That means it is not locked to +# poetry.lock; it resolves compatible versions at build time. +FROM python:3.11-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + HF_HOME=/home/sentinel/.cache/huggingface + +# build-essential lets pip compile any dependency that isn't shipped as a +# pre-built wheel. It can be dropped if every dependency installs from a wheel. +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install the CPU-only torch FIRST, from PyTorch's dedicated CPU index. Doing +# this before installing sentinel means the later install sees a compatible +# torch already present and won't replace it with the default (GPU) build. +RUN pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.3.0,<3.0.0" + +# Copy the metadata + source needed to build/install the package. +COPY pyproject.toml README.md ./ +COPY src ./src + +# Install sentinel plus the `sbert` extra (sentence-transformers). torch is +# already satisfied by the CPU build above, so it stays CPU-only. Other deps +# come from regular PyPI. +RUN pip install ".[sbert]" + +# Copy the remaining source (examples, tests, docs, etc.). +COPY . . + +# Run as a non-root user for safety. +RUN useradd --create-home sentinel \ + && mkdir -p "$HF_HOME" \ + && chown -R sentinel:sentinel /app /home/sentinel +USER sentinel + +# By default, run the beginner demo end-to-end. Override this at `docker run` +# time to run your own script, e.g.: +# docker run --rm sentinel:cpu python examples/Example_Threshold_Script.py +CMD ["python", "examples/beginner_demo.py"] diff --git a/README.md b/README.md index 8cfe2ec..0d25833 100644 --- a/README.md +++ b/README.md @@ -55,16 +55,34 @@ same way on any machine that has [Docker](https://docs.docker.com/get-docker/). ### 1. Build the image -From the repository root (the folder containing the `Dockerfile`): +There are two Dockerfiles so you can pick the one that fits your needs: + +| File | torch build | Approx. image size | Use when | +|------|-------------|--------------------|----------| +| `Dockerfile` (default) | GPU (CUDA) | ~6–8 GB | You want to run on a GPU, or want the fully pinned (`poetry.lock`) install | +| `Dockerfile.cpu` | CPU-only | ~1.5–2.5 GB | You just need CPU inference (the common case) — much smaller and faster to pull | + +Sentinel's own code runs on CPU, so for most people `Dockerfile.cpu` is the +better choice. The default `Dockerfile` pulls in the full NVIDIA CUDA libraries +(several GB) that are only useful if you actually have a GPU. + +From the repository root, build whichever you want: ```bash +# Default (GPU-capable) image, tagged "sentinel" docker build -t sentinel . + +# Smaller CPU-only image, tagged "sentinel:cpu" +docker build -f Dockerfile.cpu -t sentinel:cpu . ``` -This installs the `sentinel` library together with the `sbert` extra +Both install the `sentinel` library together with the `sbert` extra (sentence-transformers + torch). The first build downloads a lot of dependencies, so it can take several minutes. +> The examples below use the `sentinel` tag. If you built the CPU image, just +> swap in `sentinel:cpu` (e.g. `docker run --rm sentinel:cpu`). + ### 2. Run the demo ```bash From 891f34a8b9a58ef3ec7fccda781171a5857a5fa9 Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:15:43 -0700 Subject: [PATCH 4/5] Tidy whitespace in pyproject.toml Normalize blank lines around the dependency sections; no dependency or configuration changes. Co-authored-by: Cursor --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d87a519..88c5a65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Linguistic" ] + [tool.poetry.dependencies] python = ">=3.9,<4.0" numpy = ">=1.21,<2.0" @@ -44,7 +45,6 @@ mypy = "^1.15.0" pre-commit = "^4.2.0" pytest-timeout = "^2.4.0" - [tool.poetry.group.docs.dependencies] furo = "^2024.8.6" sphinx-copybutton = "^0.5.2" From c0a87b9938356ae9a38f568943c575fc63a714fa Mon Sep 17 00:00:00 2001 From: Wei Xiao <11197323+wxiao0421@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:51:29 -0700 Subject: [PATCH 5/5] update README.md --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0d25833..36c81e3 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,22 @@ There are two Dockerfiles so you can pick the one that fits your needs: | File | torch build | Approx. image size | Use when | |------|-------------|--------------------|----------| -| `Dockerfile` (default) | GPU (CUDA) | ~6–8 GB | You want to run on a GPU, or want the fully pinned (`poetry.lock`) install | -| `Dockerfile.cpu` | CPU-only | ~1.5–2.5 GB | You just need CPU inference (the common case) — much smaller and faster to pull | +| `Dockerfile` (default) | GPU (CUDA) on x86_64 Linux; CPU on arm64 | ~6–8 GB on x86_64 Linux; ~1 GB on arm64 | You want to run on a GPU, or want the fully pinned (`poetry.lock`) install | +| `Dockerfile.cpu` | CPU-only (all architectures) | ~1.5–2.5 GB | You just need CPU inference (the common case) — much smaller and faster to pull | + +> **Note on image size and architecture.** The sizes above assume you build on +> an **x86_64 (amd64) Linux** host. The default `Dockerfile` only pulls in the +> full NVIDIA CUDA libraries (several GB) there, because `poetry.lock` marks +> those packages as x86_64-Linux-only. If you build on **Apple Silicon (arm64)** +> — e.g. an M-series Mac — Docker produces an arm64 image, the CUDA packages are +> skipped, and the default image is CPU-only and much smaller (~1 GB). In that +> case both Dockerfiles end up CPU-only and similar in size. `Dockerfile.cpu` +> installs the CPU build of torch explicitly, so it is guaranteed CPU-only on +> **every** architecture (including x86_64 Linux). Sentinel's own code runs on CPU, so for most people `Dockerfile.cpu` is the -better choice. The default `Dockerfile` pulls in the full NVIDIA CUDA libraries -(several GB) that are only useful if you actually have a GPU. +better choice. On x86_64 Linux the default `Dockerfile` pulls in the full NVIDIA +CUDA libraries (several GB) that are only useful if you actually have a GPU. From the repository root, build whichever you want: