Metadata-Version: 2.4
Name: pytrustful-ai
Version: 0.1.0
Summary: Offline-first multilingual moderation MCP server with bundled model
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: mcp>=2
Requires-Dist: model2vec
Requires-Dist: alt-profanity-check
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"

# pytrustful-ai

Offline-first, multilingual content moderation for AI agents — exposed as a
[Model Context Protocol](https://modelcontextprotocol.io) server with named
presets and runtime-extensible custom checks.

`pip install pytrustful-ai` and you get a working moderation MCP with the model
already bundled. No API keys, no first-run downloads, no network calls at
runtime.

## Why

Most moderation options for agents fall into one of three traps:

- **API-only** (OpenAI Moderation, etc.) — requires a key, phones home, per-request cost.
- **Blacklist-only** — fast but blind to paraphrase, sarcasm, and non-English.
- **Heavy classifier** — accurate but a 500 MB model you can't ship inside a wheel.

`pytrustful-ai` splits the job into layers:

1. **Keyword layer** — runs on every request. Backed by `profanity_check` (English, fast) or `glin-profanity` (25+ languages) if installed.
2. **Semantic layer** — a quantized [Model2Vec](https://github.com/MinishLab/model2vec) static embedding (~62 MB) compared against safe/toxic anchors. Fires only when the keyword layer passes.

Result: sub-20 ms typical latency, English + many other languages, zero network dependency once installed.

## Install

```bash
pip install pytrustful-ai
```

Optional multilingual keyword backend:

```bash
pip install pytrustful-ai[glin]
```

## Use as an MCP server

Add to your MCP client config (Claude Desktop, Cursor, Windsurf, etc.):

```json
{
  "mcpServers": {
    "pytrustful-ai": {
      "command": "pytrustful-ai"
    }
  }
}
```

The server exposes one tool:

```
moderate(text: str, preset: str = "strict", custom_presets: dict | None = None)
```

## Use as a Python library

```python
from pytrustful_ai import run_check

result = run_check("you absolute idiot", preset="strict")
# {
#   "allowed": False,
#   "preset": "strict",
#   "triggered": [{"layer": "semantic", "toxic_probability": 0.9805}],
#   "reason": "Preset 'strict' triggered",
# }
```

## Presets

| Preset      | Layers        | Threshold | Use when                                            |
| ----------- | ------------- | --------- | --------------------------------------------------- |
| `profanity` | keyword only  | —         | You only want a fast block on hard profanity        |
| `toxicity`  | keyword + semantic | 0.60 | Balanced: catches subtle insults without overblocking |
| `strict`    | keyword + semantic | 0.40 | Public-facing agents, minors, compliance contexts    |

`run_check` returns `allowed: True` or `False`, plus a `triggered` array you can
log, route to human review, or use to explain the decision to the user.

## Custom presets

You do not have to edit the package to add rules. Pass presets at call time:

```python
run_check(
    text="internal salary figures attached",
    preset="paranoid",
    custom_presets={
        "paranoid": {"fast_only": False, "threshold": 0.15},
    },
)
```

Custom presets **merge over** the defaults — pass `strict` with a different
threshold to override it, or a new key to add one. Pass `custom_presets={}` to
use only the built-ins.

## Preset schema

Each preset is a small dict:

```python
{
    "fast_only": bool,   # True = run keyword layer only, skip the semantic model
    "threshold": float,  # Semantic-layer cutoff in [0.0, 1.0]. Ignored if fast_only.
}
```

## Offline guarantee

The quantized model (int8) and tokenizer are installed inside the wheel under
`pytrustful_ai/models/vtx-mod-1/`. `StaticModel.from_pretrained()` loads from
that local path and does not contact Hugging Face.

To prove it after install:

```bash
python - <<'PY'
from pytrustful_ai import run_check
print(run_check("hello", "strict"))
PY
```

Run that on a machine with the network disconnected — it will still print a
result.

## Configuration

| Env var                    | Default              | Meaning                                      |
| -------------------------- | -------------------- | -------------------------------------------- |
| `PYTRUSTFUL_MODEL_ID`      | `VTXAI/VTX-MOD-1`    | Model ID used by `scripts/bundle_model.py`   |
| `PYTRUSTFUL_QUANTIZATION`  | `int8`               | `int8`, `float16`, or `""` (skip)            |

These affect the **bundle** step. At runtime the bundled files are used directly
and env vars are ignored.

## Development

```bash
git clone https://github.com/<you>/pytrustful-ai
cd pytrustful-ai
python -m venv .venv && source .venv/bin/activate

pip install -e ".[dev]"
python scripts/bundle_model.py          # downloads + quantizes the model
pytest -v
```

Rebuild artifacts:

```bash
rm -rf dist/ build/ src/*.egg-info
python -m build
ls -lh dist/
```

Both artifacts must stay **under 100 MB** — PyPI's per-file limit. The CI
workflow enforces this with a guard step.

## Publishing

Tagging `v*` triggers `.github/workflows/workflow.yml`, which runs:

1. `test` — pytest with fake engine, no model download
2. `build` — bundle model, build wheel, verify size + contents
3. `publish-to-pypi` — Trusted Publishing via OIDC

First-time setup: add a pending publisher on
<https://pypi.org/manage/account/publishing/> with:

- Owner: your GitHub user/org
- Repository: `pytrustful-ai`
- Workflow: `workflow.yml`
- Environment: `pypi`

## Model attribution

The bundled model is [VTX-MOD-1](https://huggingface.co/VTXAI/VTX-MOD-1)
by VTXAI, redistributed under its model license. See `src/pytrustful_ai/models/vtx-mod-1/README.md`
for the original card.

## Limitations

- **English-leaning keyword layer by default.** `profanity_check` is trained on English. Install `[glin]` for 25+ language coverage.
- **Thresholds are heuristics.** 0.4 / 0.6 are starting points. Tune `custom_presets` for your domain.
- **No image or audio moderation.** Text only.
- **Quantization trades a little accuracy for size.** If you need fp32, run `PYTRUSTFUL_QUANTIZATION="" python scripts/bundle_model.py` and expect a ~150 MB wheel — over PyPI's limit, so host it elsewhere.

## License

MIT
