Detect, anonymize and rehydrate sensitive data before it reaches OpenAI, Anthropic or any LLM provider. 55+ languages, domain-aware keep rules, GDPR-ready vault — drop into your stack in 3 lines of code.
from privacy_firewall import create_firewall
# Create a domain-aware firewall
firewall = create_firewall("healthcare")
result = firewall.process(
text="Patient John Doe, SSN 123-45-6789,
diagnosed with hypertension.",
context={...},
)
# → "Patient PERSON_1, [REDACTED], diagnosed
# with hypertension. Prescribed lisinopril 10mg."
# Medical terms KEPT. PII is stripped.PII Firewall acts as a transparent privacy layer between your application and any LLM — with zero changes to your existing prompt logic.
"Patient Ana García, DNI 12345678A, diagnosed with hypertension."
Raw text with PII enters your system from the user or another service.
PERSON ── Ana García NATIONAL_ID ── 12345678A DIAGNOSIS ── hypertension ✓ (keep)
One or more detection backends (regex, Presidio, GLiNER, Transformers) identify PII entities. Domain rules decide what to keep.
"Patient PERSON_1, NATIONAL_ID_1, diagnosed with hypertension."
Entities are replaced according to their disposition: pseudonymize, redact, mask, generalize, or suppress. A vault stores reversible mappings.
LLM never sees real personal data.
The sanitized prompt is forwarded to any LLM provider — OpenAI, Anthropic, Mistral, local models, etc.
"Patient Ana García, DNI 12345678A, diagnosed with hypertension."
The vault restores original values in the model's response. The end-user sees real names — the LLM never did.
"Patient Ana García, DNI 12345678A, diagnosed with hypertension."
Raw text with PII enters your system from the user or another service.
PERSON ── Ana García NATIONAL_ID ── 12345678A DIAGNOSIS ── hypertension ✓ (keep)
One or more detection backends (regex, Presidio, GLiNER, Transformers) identify PII entities. Domain rules decide what to keep.
"Patient PERSON_1, NATIONAL_ID_1, diagnosed with hypertension."
Entities are replaced according to their disposition: pseudonymize, redact, mask, generalize, or suppress. A vault stores reversible mappings.
LLM never sees real personal data.
The sanitized prompt is forwarded to any LLM provider — OpenAI, Anthropic, Mistral, local models, etc.
"Patient Ana García, DNI 12345678A, diagnosed with hypertension."
The vault restores original values in the model's response. The end-user sees real names — the LLM never did.
Anonymization happens in your infrastructure. Real values never leave your trust boundary. Rehydration runs after the model responds, so end-users still see meaningful output.
Built for production. Designed for compliance. Zero compromise on LLM utility.
Language is detected automatically per message with thread-level caching for zero latency after the first call. Locale-specific patterns for Spanish DNI, US SSN, French INSEE, German Steuernummer, Italian CF, Portuguese NIF, and global fallbacks.
A secure in-memory (or SQLite) vault stores original↔token mappings per tenant/case/thread. PERSON_1 always maps back to the same real name within a conversation. Rehydration is automatic.
Medical terms (diagnoses, medications, procedures) are kept — not redacted — in the healthcare profile. Finance keeps amounts. Legal keeps case numbers. The LLM still receives the context it needs.
Every call produces a TraceRecord with detected entities, their types, confidence scores, applied replacements, and residual PII warnings. Build compliance dashboards or feed your SIEM.
A single API call wipes all vault mappings for a case or thread, satisfying Art. 17 GDPR right to erasure. Per-tenant isolation ensures data never crosses customer boundaries.
Works with OpenAI, Anthropic, Mistral, local models, or any HTTP LLM endpoint. Deploy as a Python SDK, FastAPI microservice, or Docker container. Node.js integration example included.
Add regex rules at runtime with add_custom_regex(). Plug in any HuggingFace NER model as a Presidio recognizer subclass. Zero config files — just code.
Pattern-based detection has effectively zero overhead. Presidio NER runs in 50–200ms. All modes are compatible with async FastAPI services and production workloads.
secure_call_stream() yields rehydrated tokens as soon as they arrive from the LLM — no buffering. Plug into SSE or WebSocket endpoints for real-time privacy-safe chat applications.
Each domain profile decides what's sensitive and what the LLM must see to do its job. Fully customizable.
Keep clinical context. Anonymize patient identifiers and account data.
"Paciente Ana García, DNI 12345678A, 43 años, hipertensión. Email: ana@clinic.es. IBAN: ES12345678. Prescripción: enalapril 10mg."
"Paciente PERSON_1, [REDACTED], [AGE_40-49], hipertensión. Email: [REDACTED]. IBAN: ACCOUNT_1. Prescripción: enalapril 10mg."
firewall = create_firewall("healthcare")From zero-dependency regex to GPU-accelerated biomedical NER. Switch with one parameter.
No extrasZero-dependency environments or fast structured-data pipelines.
[presidio, langdetect]General-purpose production workloads with NER requirements.
[presidio, langdetect]High-risk domains where maximum recall matters more than latency.
[gliner]Custom entity types without labeled training data.
[transformers]Clinical NLP where biomedical entity accuracy is critical.
[opf]Environments needing a lightweight yet precise token classifier without Presidio.
[opf]Maximum precision on unstructured free-form text using NVIDIA's fine-tuned model.
Each entity type in a domain profile gets a disposition — the precise transformation applied when that entity is detected.
| Action | Result | Reversible |
|---|---|---|
| KEEP | Pass through unchanged | N/A |
| PSEUDONYMIZE | Replace with stable token + vault | ✅ Yes |
| REDACT | Replace with constant marker | ❌ No |
| GENERALIZE | Keep category, drop precision | ❌ No |
| MASK | Partial reveal | ❌ No |
| HASH | SHA-256 one-way hash | ❌ No |
| SUPPRESS | Remove entirely | ❌ No |
Works with any LLM provider. Wrap any callable — no SDK lock-in.
from openai import OpenAI
from privacy_firewall import create_firewall
client = OpenAI()
firewall = create_firewall("healthcare", detector_backend="presidio")
context = {"tenant_id": "acme", "case_id": "c1", "thread_id": "t1", "actor_id": "u1"}
def openai_llm(prompt: str) -> str:
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# PII is stripped before reaching OpenAI, restored in the reply
result = firewall.secure_call(text=user_input, context=context, llm_client=openai_llm)
print(result.final_text) # real names restoredNo SDK lock-in — wrap any HTTP LLM endpoint, local model with Ollama, or LiteLLM proxy the same way.
Language is detected automatically per message with thread-level caching — zero latency after the first call. Locale-specific patterns ensure country IDs and formats are recognized correctly.
# Auto-detect (default)
firewall = create_firewall("healthcare")
# Force Spanish, skip langdetect dependency
firewall = create_firewall("healthcare", language="es")GDPR Art. 17 right to erasure, tenant isolation, and full audit traces — out of the box.
firewall.forget() wipes all vault mappings for a thread or case. After deletion, rehydration will not restore any original values for that scope.
deleted = firewall.forget(
tenant_id="hospital-001",
case_id="patient-123",
thread_id="consultation-1",
)
# → 14 mappings deletedToken mappings are scoped by tenant_id. The same PERSON_1 token in different tenants never shares a mapping. Hard isolation at the data layer.
context = {
"tenant_id": "hospital-001", # hard boundary
"case_id": "patient-123",
"thread_id": "consultation-1",
"actor_id": "doctor-456",
}Every call produces a TraceRecord with entity types, confidence scores, and applied replacements — ready for your compliance dashboard or SIEM.
result.trace.detected_entities
# → [{type: "PERSON", text: "Ana García",
# confidence: 0.97}, ...]
result.trace.replacements
# → [{"original": "Ana García",
# "token": "PERSON_1"}, ...]Three steps from install to your first anonymized LLM call.
pip install "pii-firewall[presidio,langdetect]"
# Download a spaCy language model
python -m spacy download en_core_web_sm # English
python -m spacy download es_core_news_sm # Spanishfrom privacy_firewall import create_firewall
# Pick a domain profile
firewall = create_firewall("healthcare") # or "finance", "legal", "generic"context = {
"tenant_id": "hospital-001",
"case_id": "patient-123",
"thread_id": "consultation-1",
"actor_id": "doctor-456",
}
# Anonymize before sending to LLM
anon = firewall.anonymize(text=user_input, context=context)
llm_response = my_llm(anon.sanitized_text)
# Rehydrate — restore real names in the response
final = firewall.rehydrate(text=llm_response, context=context)
print(final) # End-user sees real valuesfrom fastapi import FastAPI
from privacy_firewall import PrivacyFirewallSDK
app = FastAPI()
sdk = PrivacyFirewallSDK.create(domain="healthcare", detector_backend="presidio")
@app.post("/privacy/sanitize")
async def sanitize(req: dict):
result = sdk.anonymize_text(text=req["text"], context=req["context"])
return {"sanitized_text": result.sanitized_text}firewall.add_custom_regex(
entity_type="EMPLOYEE_ID",
regex=r"\bEMP-\d{6}\b",
locales=["GLOBAL"],
confidence=0.95,
context_words=["employee", "staff"],
disposition_action="redact",
)deleted = firewall.forget(
tenant_id="hospital-001",
case_id="patient-123",
thread_id="consultation-1",
)
print(f"Deleted {deleted} mappings")
# Art. 17 GDPR right to erasure satisfied