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
24 changes: 24 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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.
# - 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"]
57 changes: 57 additions & 0 deletions Dockerfile.cpu
Original file line number Diff line number Diff line change
@@ -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"]
88 changes: 87 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,92 @@ 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

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) 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. 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:

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

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
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
Expand Down Expand Up @@ -135,6 +221,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

Expand Down Expand Up @@ -166,7 +253,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

Expand Down
77 changes: 77 additions & 0 deletions examples/beginner_demo.py
Original file line number Diff line number Diff line change
@@ -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}")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
Loading