Local-first safe AI tools: redact-proxy + invoice-to-csv
  • Python 98.3%
  • Dockerfile 1.7%
Find a file
jonah 9172286d48
Some checks failed
CI / checks (push) Failing after 1m9s
CI / policy (push) Has been skipped
CI / python (push) Successful in 18s
Merge pull request 'docs: point AGENTS.md at the vault hub' (#5) from docs/vault-hub into main
Reviewed-on: #5
2026-10-06 15:11:40 +02:00
.forgejo/workflows feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
apps/redact-proxy fix: 502 on a dropped error body, terminate [DONE] ourselves 2026-10-06 14:28:53 +02:00
data/testsets chore: land 0.2.0 on main (PR #2 merged into its stacked base) (#3) 2026-10-05 21:23:22 +02:00
packages/core feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
.dockerignore feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
.gitignore chore: land 0.2.0 on main (PR #2 merged into its stacked base) (#3) 2026-10-05 21:23:22 +02:00
.importlinter chore: land 0.2.0 on main (PR #2 merged into its stacked base) (#3) 2026-10-05 21:23:22 +02:00
.pre-commit-config.yaml feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
.python-version feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
AGENTS.md docs: point AGENTS.md at the vault hub 2026-10-06 14:29:44 +02:00
BRIEF.md feat: streaming support, restore reasoning_content (0.3.0) 2026-10-05 21:51:34 +02:00
CHANGELOG.md fix: 502 on a dropped error body, terminate [DONE] ourselves 2026-10-06 14:28:53 +02:00
CLAUDE.md feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
docker-compose.yml feat: pii_core validators/recognizers and non-streaming redact-proxy MVP 2026-10-04 13:05:08 +02:00
LICENSE chore: bootstrap repo with brief, license, readme 2026-10-04 12:42:55 +02:00
pyproject.toml feat: streaming support, restore reasoning_content (0.3.0) 2026-10-05 21:51:34 +02:00
README.md feat: streaming support, restore reasoning_content (0.3.0) 2026-10-05 21:51:34 +02:00
ruff.toml chore: land 0.2.0 on main (PR #2 merged into its stacked base) (#3) 2026-10-05 21:23:22 +02:00
uv.lock feat: streaming support, restore reasoning_content (0.3.0) 2026-10-05 21:51:34 +02:00
VERSION feat: streaming support, restore reasoning_content (0.3.0) 2026-10-05 21:51:34 +02:00

safe-ai-tools

Free, MIT-licensed, local-first AI tools for real business work.

Tool Status
redact-proxy Redacts PII from prompts, forwards to any OpenAI-compatible upstream, restores values in the reply.
invoice-to-csv Planned.

Shared validators and recognizers (NL, DE, EN) live in packages/core.

redact-proxy

An OpenAI-compatible /v1/chat/completions endpoint. Your client talks to it instead of the model; personal data never reaches the upstream.

client ──► redact-proxy ──► Ollama / OpenAI / Anthropic-compatible / ...
  "Jan, BSN 111222333"   "[PERSON_1], BSN [NL_BSN_1]"
client ◄── restores ◄──────  reply mentioning [PERSON_1]

Run it

docker compose up --build            # proxy on http://127.0.0.1:8080

The default config forwards to an Ollama on your host (http://host.docker.internal:11434/v1). To change the upstream, language or allow-list, copy apps/redact-proxy/config.example.yaml, edit it, and run REDACT_PROXY_CONFIG_FILE=./my-config.yaml docker compose up. No Ollama? docker compose --profile ollama up starts one (set upstream_url: http://ollama:11434/v1).

Without Docker: uv sync --all-packages && uv run python -m spacy download nl_core_news_lg && REDACT_PROXY_CONFIG=config.yaml uv run python -m redact_proxy.

Use it

Change only the base URL in any OpenAI-compatible client:

from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="...")

See what would be redacted without forwarding anything (this doubles as the demo):

curl -s localhost:8080/v1/chat/completions -H 'X-Redact-Dry-Run: 1' -H 'content-type: application/json' \
  -d '{"model":"m","messages":[{"role":"user","content":"Jan de Vries, BSN 111222333, jan@example.nl"}]}'
{"dry_run": true,
 "redacted_messages": [{"role": "user", "content": "[PERSON_1], BSN [NL_BSN_1], [EMAIL_1]"}],
 "entity_counts": {"PERSON": 1, "NL_BSN": 1, "EMAIL": 1}}

What it detects

Kind How Reliability
NL: BSN (11-proef), IBAN (mod-97), BTW-nummer, KvK, postcode, phone regex + checksum Strong where a checksum exists. KvK and postcode have none, so see the limits below.
DE: Steuer-ID, USt-IdNr, IBAN, PLZ, phone regex + checksum Same.
Common: email, credit card (Luhn) regex + checksum Strong.
Names and addresses spaCy NER (nl, de, en) The weak spot. NER misses names and misreads unusual ones. Treat it as risk reduction, not a guarantee.

Known limits in this release:

  • KvK numbers and German PLZ are only caught next to a context word (e.g. "KvK-nummer 12345678"). A bare postcode like 3511 AB is currently not redacted without one.
  • Streaming (stream: true) is supported. If the upstream cuts a stream off mid-reply, the client gets an SSE error event and the stream ends.
  • Tool-call arguments and function names are not redacted.
  • A literal placeholder already in your text (e.g. [PERSON_1]) can be swapped for a restored value in the reply.
  • Language is set in config, not detected per request.
  • Image, audio and file content parts are rejected with a 400 (they cannot be redacted). Set allow_non_text_parts: true to forward them untouched.
  • Credit cards: recall is about 63-76% on the synthetic set, because Presidio's card pattern misses Mastercard 2-series, 15-digit and 19-digit numbers.

Measured accuracy

Full pipeline (structured recognizers + spaCy *_core_news_md / en_core_web_md), 1,000 synthetic documents per language (seed 1, data/testsets/generate.py). Recall = gold spans found with the right type. Leak rate = share of PII characters left unredacted.

Entity NL recall DE recall EN recall Precision (NL / DE / EN)
Email, IBAN 100% 100% 100% 100%
BSN, BTW, KvK (NL) 100% n/a n/a 100%
Steuer-ID, USt-IdNr (DE) n/a 100% n/a 100%
Credit card 63% 69% 76% 100%
Phone 78% 83% 88% 86% / 91% / 98%
Postcode / PLZ 33% 47% n/a 100%
Person 91% 97% 93% 80% / 85% / 95%
Location 68% 95% 78% 73% / 92% / 98%
Leak rate 16.5% 7.4% 8.8%

Read this with care:

  • The test data is synthetic. Faker names and cities are cleaner than real text, so real-world name recall will be lower. These numbers are an upper bound, not a promise.
  • KvK is 100% only because the test documents put a KvK context word next to the number; a bare 8-digit number is not caught.
  • Precision is measured on the analyzer output before the anonymizer resolves overlapping hits, so it is slightly pessimistic.
  • The table predates the six hand-written NL edge cases now included in the first lines of each set; rerun to refresh it.
  • Reproduce: uv run python data/testsets/generate.py --n 1000 && uv run python -m redact_proxy.eval --lang nl --backend spacy-md --limit 1000.

How your data is handled

  • Local by default. The proxy runs on your machine. The only outbound call is the one to the upstream you configure.
  • Mapping lives in memory only, per request, and is dropped when the request ends. Nothing is written to disk.
  • Audit log records entity types and counts only ({"PERSON": 1, "IBAN": 2}), never values.
  • Dry-run mode forwards nothing.
  • This reduces how much personal data reaches a model. It does not make a workflow "GDPR compliant", and it does not catch everything.

Layout

packages/core/          shared validators + Presidio recognizers
apps/redact-proxy/      the proxy
data/testsets/          synthetic test-set generator

Develop

uv sync --all-packages --dev
uv run pytest -m "not integration"      # hermetic
uv run pytest -m integration            # needs a spaCy nl model installed
uv run python -m redact_proxy.eval --lang nl --backend patterns   # accuracy harness (see above)
uv run ruff check . && uv run lint-imports

Want this tailored to your stack?

Installation, tuning to your invoices and entities, and maintenance: Code & Canvas.

MIT licensed. See LICENSE.