PromptVeil
Privacy and safety middleware for LLM applications.
PromptVeil intercepts prompts before they reach an LLM API, detects sensitive information (PII, secrets), applies configurable policies (mask · block · allow), and produces structured JSON audit logs for compliance.
Features
| Capability | Detail |
|---|---|
| PII detection | Email, phone, credit card, API keys (regex, extensible) |
| India PII | Aadhaar (Verhoeff checksum), PAN, GSTIN, IFSC, Passport, Voter ID |
| US PII | SSN (segment-validated), US Passport, EIN |
| Policy engine | YAML-driven rules: mask, block, allow; per-rule confidence thresholds |
| Masking | Type-aware redaction (domain preserved for email, last-4 for cards) |
| Blocking | Raises SensitiveDataError before the prompt is forwarded |
| Audit logging | Structured JSON lines — raw values never logged |
| Confidence scoring | Each detected entity carries a confidence float; rules can require a minimum |
| Rich scan results | scan_detailed() returns ScanResult with risk_score, risk_level, and per-entity EntityResult |
| Compliance profiles | One-line activation of india_dpdpa, eu_gdpr, or us_hipaa rule packs |
| Pseudonymization vault | scan_with_vault() replaces PII with reversible <TYPE_N> tokens; restore after LLM response |
| OpenAI adapter | wrap_openai(client, shield) patches chat.completions.create in-place |
| LangChain adapter | get_langchain_callback() returns a BaseCallbackHandler that scans prompts automatically |
| Plug-in detectors | Subclass BaseDetector to add spaCy, ML models, custom patterns |
| FastAPI middleware | Drop-in Starlette/FastAPI middleware (optional extra) |
Installation
# Core (only requires PyYAML)
pip install promptveil
# With FastAPI middleware support
pip install "promptveil[fastapi]"
# With OpenAI adapter
pip install "promptveil[openai]"
# With LangChain adapter
pip install "promptveil[langchain]"
# Everything
pip install "promptveil[fastapi,openai,langchain,dev]"
# Development (includes pytest)
pip install "promptveil[dev]"
Requires Python ≥ 3.10.
Quick Start
from promptveil import Shield
shield = Shield(config_path="policy.yaml")
safe = shield.scan("Bill me at alice@example.com, card 4111-1111-1111-1111")
# ↑ raises SensitiveDataError — credit card is blocked by default policy
safe = shield.scan("Contact alice@example.com for the invoice.")
# → "Contact a***@example.com for the invoice."
Activate a compliance profile in one line
# policy.yaml
profile: india_dpdpa # or eu_gdpr / us_hipaa
# optional rule overrides go here under `rules:`
shield = Shield(config_path="policy.yaml") # profile detectors loaded automatically
Rich scan result with risk scoring
result = shield.scan_detailed("Call me on +91 98765 43210")
print(result.risk_score) # e.g. 0.42
print(result.risk_level) # "MEDIUM"
print(result.safe_text) # sanitised prompt
Reversible pseudonymization
from promptveil import PseudonymVault
vault = PseudonymVault()
pseudo_text, vault = shield.scan_with_vault("Email alice@example.com", vault=vault)
# pseudo_text → "Email <EMAIL_1>"
llm_response = call_llm(pseudo_text) # LLM sees tokens, not PII
final = vault.restore(llm_response) # restore originals in the reply
Drop-in OpenAI integration
import openai
from promptveil.adapters import wrap_openai
client = openai.OpenAI()
wrap_openai(client, shield=shield) # patches client in-place
# All subsequent calls are automatically scanned:
client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
Policy Configuration (policy.yaml)
version: "1.0"
default_action: allow # fallback: mask | block | allow
# Optional: activate a built-in compliance profile (india_dpdpa | eu_gdpr | us_hipaa)
# profile: eu_gdpr
rules:
- type: email
action: mask # a***@example.com
min_confidence: 0.8 # optional: only apply rule when confidence ≥ this value
- type: phone
action: mask # ***-***-5309
- type: credit_card
action: block # raises SensitiveDataError
- type: api_key
action: block # raises SensitiveDataError
Built-in entity types: email, phone, credit_card, api_key,
in_aadhaar, in_pan, in_phone, in_passport, in_voter_id, in_ifsc, in_gstin,
ssn, us_passport, us_ein.
Custom detectors can introduce any additional type labels.
Compliance Profiles
Profiles bundle a pre-configured PolicyConfig and the appropriate detectors
for a jurisdiction. Activate via the YAML profile: key or programmatically:
from promptveil.profiles import load_profile
profile = load_profile("us_hipaa") # or india_dpdpa / eu_gdpr
shield = Shield(config=profile.policy, detectors=profile.detectors)
| Profile | Detectors | Key rules |
|---|---|---|
india_dpdpa |
RegexDetector + IndiaDetector |
Aadhaar/PAN/Passport → BLOCK; phone/GSTIN/IFSC → MASK |
eu_gdpr |
RegexDetector |
email/phone → MASK; credit card/API key → BLOCK |
us_hipaa |
RegexDetector + USDetector |
SSN → BLOCK; email/phone/passport/EIN → MASK |
API Reference
Shield(config_path=…, config=…, detectors=…, logger=…)
| Param | Type | Description |
|---|---|---|
config_path |
str | Path |
Path to a YAML policy file |
config |
PolicyConfig |
Pre-built config (alternative to config_path) |
detectors |
list[BaseDetector] |
Custom detector list; overrides profile detectors when provided |
logger |
AuditLogger |
Custom audit logger (default: JSON → stdout) |
shield.scan(prompt: str) -> str
Scans the prompt, applies policy rules, and returns a sanitised string.
Raises SensitiveDataError if a block rule fires.
shield.scan_response(response: str) -> str
Same as scan but semantically applied to LLM output.
shield.scan_detailed(prompt: str) -> ScanResult
Like scan, but returns a ScanResult dataclass instead of a plain string:
@dataclass(frozen=True)
class ScanResult:
safe_text: str # sanitised prompt
entities: tuple[EntityResult] # per-entity detail
risk_score: float # 0.0 – 1.0 probabilistic risk
risk_level: str # NONE | LOW | MEDIUM | HIGH | CRITICAL
was_mutated: bool # True if any masking was applied
risk_level thresholds: NONE = 0.0, LOW < 0.30, MEDIUM < 0.60, HIGH < 0.85, CRITICAL ≥ 0.85.
shield.scan_with_vault(prompt: str, vault=None) -> tuple[str, PseudonymVault]
Replaces each masked/blocked entity with a reversible token (<EMAIL_1>, <PHONE_1>, …).
Pass the returned PseudonymVault to vault.restore(text) to swap tokens back after the LLM response.
shield.add_detector(detector: BaseDetector)
Register an additional detector at runtime.
PseudonymVault
| Method | Description |
|---|---|
pseudonymize(text, entities) |
Replace entity spans with tokens (called internally by scan_with_vault) |
restore(text) |
Swap all tokens back to their original values |
get_token(value) |
Look up the token assigned to an original value |
token_map |
Read-only copy of the current {token: original_value} mapping |
clear() |
Reset the vault |
len(vault) |
Number of unique originals stored |
Programmatic Config (no YAML file)
from promptveil import Shield
from promptveil.policy.models import Action, PolicyConfig, Rule
config = PolicyConfig(
rules=[
Rule(type="email", action=Action.MASK, min_confidence=0.8),
Rule(type="api_key", action=Action.BLOCK),
],
default_action=Action.ALLOW,
)
shield = Shield(config=config)
min_confidence is optional (defaults to 0.0). When a detector reports
confidence < min_confidence for an entity, the rule is skipped and
default_action applies instead.
Custom Detectors
Subclass BaseDetector to add any detection logic:
import re
from promptveil.detector.base import BaseDetector, DetectedEntity
class SSNDetector(BaseDetector):
_PATTERN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b")
def detect(self, text: str) -> list[DetectedEntity]:
return [
DetectedEntity("ssn", m.group(), m.start(), m.end())
for m in self._PATTERN.finditer(text)
]
@property
def supported_types(self) -> list[str]:
return ["ssn"]
shield = Shield(
config=PolicyConfig(rules=[Rule(type="ssn", action=Action.MASK)]),
detectors=[SSNDetector()],
)
FastAPI Middleware
from fastapi import FastAPI
from promptveil import Shield
from promptveil.middleware import PromptVeilMiddleware
app = FastAPI()
shield = Shield(config_path="policy.yaml")
app.add_middleware(PromptVeilMiddleware, shield=shield, prompt_field="prompt")
POST requests with Content-Type: application/json that contain the
prompt_field key are intercepted automatically. Blocked prompts receive a
400 response with a structured error body.
LangChain Adapter
from promptveil.adapters import get_langchain_callback
CallbackClass = get_langchain_callback() # lazy-imported; requires langchain-core
callback = CallbackClass(shield)
llm.invoke(prompt, config={"callbacks": [callback]})
The callback implements on_llm_start (string prompts) and
on_chat_model_start (chat messages), scanning and mutating content
in-place before it reaches the model.
Audit Log Format
Each log line is a self-contained JSON object:
{
"event": "prompt_scan",
"timestamp": "2026-05-03T07:34:19.800456+00:00",
"prompt_length": 44,
"detections": 1,
"entities": [
{
"entity_type": "email",
"action": "mask",
"value_preview": "a***@example.com",
"position": {"start": 26, "end": 43}
}
]
}
Raw sensitive values are never stored in logs.
Project Structure
promptveil/
├── __init__.py # Public API: Shield, exceptions
├── shield.py # Main Shield class — orchestration
├── exceptions.py # SensitiveDataError, ConfigurationError
├── config/
│ └── loader.py # YAML loader & validator
├── detector/
│ ├── base.py # BaseDetector ABC + DetectedEntity dataclass
│ └── regex_detector.py # Built-in regex patterns (email/phone/card/key)
├── policy/
│ ├── models.py # Action enum, Rule, PolicyConfig dataclasses
│ └── engine.py # PolicyEngine — entity → action lookup
├── actions/
│ ├── masker.py # Per-type masking + apply_masks()
│ └── blocker.py # block() — raises SensitiveDataError
├── logger/
│ └── audit_logger.py # Structured JSON audit logger
└── middleware/
└── fastapi_middleware.py # Optional Starlette/FastAPI middleware
policy.yaml # Example policy (mask email/phone, block card/key)
example_usage.py # Runnable demo
Running Tests
uv run pytest -v
# or: python -m pytest -v
234 tests, < 1 s.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file promptveil-0.1.0.tar.gz.
File metadata
- Download URL: promptveil-0.1.0.tar.gz
- Upload date:
- Size: 49.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed1f6e1492fc3e2ec7a72478714e9649dcb1e096201e23a9db7ed6b901ba5414
|
|
| MD5 |
3e614b534a56ceeea8fc4c922613c071
|
|
| BLAKE2b-256 |
7a2efa84681ed1d27fb9bf2843a75b306510e362ccb46f3dcf5fda7f8122e323
|
Provenance
The following attestation bundles were made for promptveil-0.1.0.tar.gz:
Publisher:
publish.yml on total-tensor-lab/PromptVeil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptveil-0.1.0.tar.gz -
Subject digest:
ed1f6e1492fc3e2ec7a72478714e9649dcb1e096201e23a9db7ed6b901ba5414 - Sigstore transparency entry: 2581737934
- Sigstore integration time:
-
Permalink:
total-tensor-lab/PromptVeil@a6851deb0b1331cddf483566588f54764453c66f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/total-tensor-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a6851deb0b1331cddf483566588f54764453c66f -
Trigger Event:
push
-
Statement type:
File details
Details for the file promptveil-0.1.0-py3-none-any.whl.
File metadata
- Download URL: promptveil-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74407ad6df04150da7a4db32fe8d2ad96d363e9ba19c126670967f1f909ad52e
|
|
| MD5 |
d1987ab36e46f49422014789a56e77e6
|
|
| BLAKE2b-256 |
5408704d0c384579c6740ac53e95bfa177455e4d89665e6cc9bdf8a853ff397d
|
Provenance
The following attestation bundles were made for promptveil-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on total-tensor-lab/PromptVeil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptveil-0.1.0-py3-none-any.whl -
Subject digest:
74407ad6df04150da7a4db32fe8d2ad96d363e9ba19c126670967f1f909ad52e - Sigstore transparency entry: 2581737948
- Sigstore integration time:
-
Permalink:
total-tensor-lab/PromptVeil@a6851deb0b1331cddf483566588f54764453c66f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/total-tensor-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a6851deb0b1331cddf483566588f54764453c66f -
Trigger Event:
push
-
Statement type: