Add PII redaction POC for secure LLM prompting.
Implements detect-redact-sanitize pipeline with regex, domain rules, and ONNX NER before the LLM boundary, plus NUnit tests and Xenovex push documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
101
scripts/download-ner-model.ps1
Normal file
101
scripts/download-ner-model.ps1
Normal file
@@ -0,0 +1,101 @@
|
||||
# Downloads dslim/bert-base-NER ONNX assets to models/ for the PII Redaction POC.
|
||||
param(
|
||||
[string]$Python = "python"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$modelsDir = Join-Path $repoRoot "models"
|
||||
$scriptPath = Join-Path $PSScriptRoot "download-ner-model.py"
|
||||
$baseUrl = "https://huggingface.co/dslim/bert-base-NER/resolve/main/onnx"
|
||||
|
||||
function Ensure-ModelsDirectory {
|
||||
New-Item -ItemType Directory -Force -Path $modelsDir | Out-Null
|
||||
}
|
||||
|
||||
function Download-HuggingFaceAsset {
|
||||
param(
|
||||
[string]$RelativePath,
|
||||
[string]$Destination
|
||||
)
|
||||
|
||||
$url = "$baseUrl/$RelativePath"
|
||||
Write-Host "Downloading $url"
|
||||
Invoke-WebRequest -Uri $url -OutFile $Destination -UseBasicParsing
|
||||
}
|
||||
|
||||
function Export-LabelsFromConfig {
|
||||
param([string]$ConfigPath, [string]$LabelsPath)
|
||||
|
||||
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||
$labelMap = @{}
|
||||
foreach ($property in $config.id2label.PSObject.Properties) {
|
||||
$labelMap[[int]$property.Name] = [string]$property.Value
|
||||
}
|
||||
|
||||
$labels = for ($index = 0; $index -lt $labelMap.Count; $index++) {
|
||||
$labelMap[$index]
|
||||
}
|
||||
|
||||
$labels | Set-Content -Path $LabelsPath -Encoding utf8
|
||||
}
|
||||
|
||||
function Download-WithPowerShell {
|
||||
Ensure-ModelsDirectory
|
||||
|
||||
$modelPath = Join-Path $modelsDir "ner-model.onnx"
|
||||
$vocabPath = Join-Path $modelsDir "vocab.txt"
|
||||
$configPath = Join-Path $modelsDir "config.json"
|
||||
$labelsPath = Join-Path $modelsDir "ner-labels.txt"
|
||||
|
||||
Download-HuggingFaceAsset -RelativePath "model.onnx" -Destination $modelPath
|
||||
Download-HuggingFaceAsset -RelativePath "vocab.txt" -Destination $vocabPath
|
||||
Download-HuggingFaceAsset -RelativePath "config.json" -Destination $configPath
|
||||
Export-LabelsFromConfig -ConfigPath $configPath -LabelsPath $labelsPath
|
||||
Remove-Item $configPath -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "NER model assets saved:"
|
||||
Write-Host " $modelPath"
|
||||
Write-Host " $vocabPath"
|
||||
Write-Host " $labelsPath"
|
||||
Write-Host ""
|
||||
Write-Host "Run from repository root:"
|
||||
Write-Host " dotnet run --project src/PiiRedaction.ConsoleApp -- --name CustomerNameOnly"
|
||||
}
|
||||
|
||||
function Download-WithPython {
|
||||
if (-not (Get-Command $Python -ErrorAction SilentlyContinue)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$pythonCommand = Get-Command $Python
|
||||
if ($pythonCommand.Source -like "*WindowsApps*") {
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host "Using Python: $($pythonCommand.Source)"
|
||||
Write-Host "Repository root: $repoRoot"
|
||||
Write-Host ""
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
& $Python $scriptPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Model download script failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Host "Repository root: $repoRoot"
|
||||
|
||||
if (-not (Download-WithPython)) {
|
||||
Write-Host "Python export unavailable; downloading pre-exported ONNX assets from Hugging Face..."
|
||||
Write-Host ""
|
||||
Download-WithPowerShell
|
||||
}
|
||||
83
scripts/download-ner-model.py
Normal file
83
scripts/download-ner-model.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download and export dslim/bert-base-NER to ONNX for the PII Redaction POC."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
MODELS_DIR = REPO_ROOT / "models"
|
||||
MODEL_ID = "dslim/bert-base-NER"
|
||||
REQUIRED_PACKAGES = ("transformers", "optimum[onnxruntime]", "onnx", "torch")
|
||||
|
||||
|
||||
def ensure_dependencies() -> None:
|
||||
try:
|
||||
import optimum.onnxruntime # noqa: F401
|
||||
import transformers # noqa: F401
|
||||
except ImportError:
|
||||
print("Installing Python dependencies (this may take a few minutes)...")
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", *REQUIRED_PACKAGES],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def export_model() -> None:
|
||||
from optimum.onnxruntime import ORTModelForTokenClassification
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = MODELS_DIR / "_export_temp"
|
||||
if temp_dir.exists():
|
||||
shutil.rmtree(temp_dir)
|
||||
temp_dir.mkdir()
|
||||
|
||||
print(f"Exporting {MODEL_ID} to ONNX...")
|
||||
model = ORTModelForTokenClassification.from_pretrained(MODEL_ID, export=True)
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
||||
|
||||
model.save_pretrained(temp_dir)
|
||||
tokenizer.save_pretrained(temp_dir)
|
||||
|
||||
onnx_files = sorted(temp_dir.glob("*.onnx"))
|
||||
if not onnx_files:
|
||||
raise FileNotFoundError("Export completed but no .onnx file was produced.")
|
||||
|
||||
target_onnx = MODELS_DIR / "ner-model.onnx"
|
||||
shutil.copy(onnx_files[0], target_onnx)
|
||||
shutil.copy(temp_dir / "vocab.txt", MODELS_DIR / "vocab.txt")
|
||||
|
||||
config_path = temp_dir / "config.json"
|
||||
with config_path.open(encoding="utf-8") as config_file:
|
||||
config = json.load(config_file)
|
||||
|
||||
id2label = config.get("id2label", {})
|
||||
labels = [id2label[str(index)] for index in range(len(id2label))]
|
||||
(MODELS_DIR / "ner-labels.txt").write_text("\n".join(labels), encoding="utf-8")
|
||||
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
print()
|
||||
print("NER model assets saved:")
|
||||
print(f" {target_onnx}")
|
||||
print(f" {MODELS_DIR / 'vocab.txt'}")
|
||||
print(f" {MODELS_DIR / 'ner-labels.txt'}")
|
||||
print()
|
||||
print("Run from repository root:")
|
||||
print(" dotnet run --project src/PiiRedaction.ConsoleApp -- --name CustomerNameOnly")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ensure_dependencies()
|
||||
export_model()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user