🕵️ Rewriting Legal Documents¶
Rewriting legal text (TAB dataset) with a domain-specific privacy goal and custom entity labels tailored for legal proceedings.
📚 What you'll learn¶
- Define domain-specific entity labels for legal text (case numbers, court names, etc.)
- Configure rewrite mode with legal-specific privacy goals
- Preview and run on court decision documents
- Triage flagged records with
needs_human_review
Tip: First time running notebooks? Start with setup instructions.
⚙️ Setup¶
- Check if your
NVIDIA_API_KEYfrom build.nvidia.com is registered for model access.- The default
build.nvidia.com(NVIDIA Build) setup is a convenient way to try Anonymizer and iterate on previews. Use of NVIDIA Build is subject to NVIDIA Build's own terms of service and privacy practices, which are separate from and independent of the NeMo Framework library. NVIDIA Build is intended for evaluation and testing purposes only and may not be used in production environments. Do not upload any confidential information or personal data when using NVIDIA Build. Your use of NVIDIA Build is logged for security purposes and to improve NVIDIA products and services. - Request and token rate limits on
build.nvidia.comvary by account and model access, and lower-volume development access can be slow for full-dataset runs. Start withpreview()on a small sample, then move to your own endpoint for production data and usage.
- The default
- Import
Detect(for custom entity labels),Rewrite, and its config classes. Anonymizer()initializes with the default model provider -- no extra config needed.configure_logging(LoggingConfig.default())keeps logs at INFO. Switch toLoggingConfig.debug()when troubleshooting.
import getpass
import os
if not os.getenv("NVIDIA_API_KEY"):
key = getpass.getpass("Enter NVIDIA_API_KEY from build.nvidia.com: ").strip()
if not key:
raise RuntimeError("NVIDIA_API_KEY is required to run these notebooks.")
os.environ["NVIDIA_API_KEY"] = key
from anonymizer import (
Anonymizer,
AnonymizerConfig,
AnonymizerInput,
Detect,
LoggingConfig,
PrivacyGoal,
Rewrite,
configure_logging,
)
configure_logging(LoggingConfig.default())
anonymizer = Anonymizer()
[11:36:11] [INFO] 🔧 Anonymizer initialized with 3 model configs
[11:36:11] [INFO] |-- 🔎 detector: gliner-pii-detector
[11:36:11] [INFO] |-- ✅ validator: gpt-oss-120b
[11:36:11] [INFO] |-- 🧩 augmenter: gpt-oss-120b
📦 Input data¶
- TAB (Text Anonymization Benchmark) legal documents -- court decisions containing names, dates, case numbers, and other legal identifiers.
LEGAL_ENTITY_LABELSdefines the domain-specific entity types to detect. This replaces the default label set with one tailored to legal text.
LEGAL_ENTITY_LABELS = [
"first_name",
"last_name",
"court_name",
"organization_name",
"company_name",
"prison_detention_facility",
"street_address",
"city",
"state",
"country",
"date",
"date_time",
"time",
"date_of_birth",
"age",
"email",
"phone_number",
"ssn",
"unique_id",
"legal_role",
"case_number",
"application_number",
"monetary_amount",
"sentence_duration",
"nationality",
]
input_data = AnonymizerInput(
source="https://raw.githubusercontent.com/NVIDIA-NeMo/Anonymizer/refs/heads/main/docs/data/TAB_legal_sample25.csv",
text_column="text",
data_summary="Legal court decisions containing personal identifiers, case numbers, and institutional references",
)
🎛️ Configure¶
Detect(entity_labels=...)overrides the default entity set with legal-specific labels. The explicit list is a strict allowlist for both detection and LLM augmentation: labels not included here are filtered out, so include every entity type you need.PrivacyGoaltells the rewriter what to protect (identifiers, case numbers, institutional references) and what to preserve (legal reasoning, statutory references, ruling structure).
config = AnonymizerConfig(
detect=Detect(
entity_labels=LEGAL_ENTITY_LABELS,
),
rewrite=Rewrite(
privacy_goal=PrivacyGoal(
protect="All personal identifiers, case numbers, court names, and institutional references that could identify parties",
preserve="Legal reasoning, procedural facts, statutory references, and the structure of the ruling",
),
risk_tolerance="minimal",
max_repair_iterations=3,
),
)
👁️ Preview¶
- Preview on a few records to check that legal entities are detected and the rewrite preserves the ruling's structure.
preview = anonymizer.preview(
config=config,
data=input_data,
num_records=3,
)
preview.display_record(0)
[11:36:12] [INFO] 👀 Preview mode: 📂 Loaded 3 records from https://raw.githubusercontent.com/NVIDIA-NeMo/Anonymizer/refs/heads/main/docs/data/TAB_legal_sample25.csv (column: 'text')
[11:36:12] [INFO] 🔍 Running entity detection on 3 records
[11:36:12] [INFO] detection labels in scope: ['age', 'application_number', 'case_number', 'city', 'company_name', 'country', 'court_name', 'date', 'date_of_birth', 'date_time', 'email', 'first_name', 'last_name', 'legal_role', 'monetary_amount', 'nationality', 'organization_name', 'phone_number', 'prison_detention_facility', 'sentence_duration', 'ssn', 'state', 'street_address', 'time', 'unique_id']
[11:37:24] [INFO] |-- 📋 Detection complete — 141 entities found across 3 records (0 failed) [71.9s]
[11:37:24] [INFO] |-- labels: date=52, court_name=35, legal_role=10, organization_name=7, nationality=7, last_name=7, country=5, first_name=5, city=5, application_number=3, date_of_birth=2, monetary_amount=1, case_number=1, sentence_duration=1
[11:37:24] [INFO] ✏️ Running rewrite pipeline
[11:40:33] [INFO] Evaluate-repair loop iteration 0: 1/3 rows need repair
[11:41:14] [INFO] Evaluate-repair loop: all rows pass at iteration 1
[11:41:14] [INFO] |-- 📋 Rewrite complete (0 failed) [229.9s]
[11:41:14] [INFO] 🎉 Pipeline complete — 3 records processed, 0 total failures
preview.display_record(1)
How to interpret leakage: Leakage is measured against the sensitivity disposition. Details marked
leave_as_ismay remain without increasingleakage_mass. If an output retains something you expected the privacy goal to protect, inspect the Entity Disposition table.
🚀 Full run¶
result.dataframehas user-facing columns: rewritten text, scores, and the review flag.- This notebook uses
risk_tolerance="minimal", which applies stricter repair and review thresholds than notebook 04.
result = anonymizer.run(config=config, data=input_data)
result.dataframe.head()
[11:41:15] [INFO] 📂 Loaded 25 records from https://raw.githubusercontent.com/NVIDIA-NeMo/Anonymizer/refs/heads/main/docs/data/TAB_legal_sample25.csv (column: 'text')
[11:41:15] [INFO] 🔍 Running entity detection on 25 records
[11:41:15] [INFO] detection labels in scope: ['age', 'application_number', 'case_number', 'city', 'company_name', 'country', 'court_name', 'date', 'date_of_birth', 'date_time', 'email', 'first_name', 'last_name', 'legal_role', 'monetary_amount', 'nationality', 'organization_name', 'phone_number', 'prison_detention_facility', 'sentence_duration', 'ssn', 'state', 'street_address', 'time', 'unique_id']
[11:45:50] [INFO] |-- 📋 Detection complete — 1277 entities found across 25 records (0 failed) [275.2s]
[11:45:50] [INFO] |-- labels: date=416, court_name=233, legal_role=169, last_name=80, organization_name=75, first_name=62, nationality=56, city=46, country=43, application_number=26, date_of_birth=25, prison_detention_facility=15, sentence_duration=13, monetary_amount=10, state=5, case_number=1, age=1, company_name=1
[11:45:50] [INFO] ✏️ Running rewrite pipeline
[12:03:11] [INFO] Evaluate-repair loop iteration 0: 15/25 rows need repair
[12:07:13] [INFO] Evaluate-repair loop iteration 1: 9/25 rows need repair
[12:09:36] [INFO] Evaluate-repair loop iteration 2: 6/25 rows need repair
[12:10:41] [INFO] |-- 📋 Rewrite complete (0 failed) [1491.6s]
[12:10:41] [INFO] 🎉 Pipeline complete — 25 records processed, 0 total failures
| text | text_rewritten | utility_score | leakage_mass | weighted_leakage_rate | any_high_leaked | needs_human_review | |
|---|---|---|---|---|---|---|---|
| 0 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.957895 | 0.0 | 0.0 | False | False |
| 1 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.983333 | 9.7 | 0.485 | True | True |
| 2 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.93125 | 21.0 | 0.954545 | True | True |
| 3 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.952941 | 0.0 | 0.0 | False | False |
| 4 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.978571 | 4.0 | 0.190476 | True | True |
result.dataframe[["text_rewritten", "utility_score", "leakage_mass", "needs_human_review"]].head()
| text_rewritten | utility_score | leakage_mass | needs_human_review | |
|---|---|---|---|---|
| 0 | PROCEDURE The case originated in an applicati... | 0.957895 | 0.0 | False |
| 1 | PROCEDURE The case originated in an applicati... | 0.983333 | 9.7 | True |
| 2 | PROCEDURE The case originated in an applicati... | 0.93125 | 21.0 | True |
| 3 | PROCEDURE The case originated in an applicati... | 0.952941 | 0.0 | False |
| 4 | PROCEDURE The case originated in an applicati... | 0.978571 | 4.0 | True |
🚩 Filter by review flag¶
- Records where automated metrics exceed thresholds are flagged for manual review.
- The repair loop stops after
max_repair_iterations; records that still need repair remain flagged for human review but are not pipeline failures. - Use this to prioritize human attention on the records that need it most.
- See Working with flagged records for guidance on diagnosing and resolving flagged records.
df = result.dataframe
flagged = df[df["needs_human_review"] == True] # noqa: E712
print(f"{len(flagged)} of {len(df)} records flagged for human review")
flagged.head()
6 of 25 records flagged for human review
| text | text_rewritten | utility_score | leakage_mass | weighted_leakage_rate | any_high_leaked | needs_human_review | |
|---|---|---|---|---|---|---|---|
| 1 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.983333 | 9.7 | 0.485 | True | True |
| 2 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.93125 | 21.0 | 0.954545 | True | True |
| 4 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.978571 | 4.0 | 0.190476 | True | True |
| 17 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.963636 | 0.99 | 0.097059 | True | True |
| 20 | PROCEDURE The case originated in an applicati... | PROCEDURE The case originated in an applicati... | 0.992857 | 4.0 | 0.266667 | True | True |
🔬 Evaluate (optional)¶
Call evaluate() to run LLM-as-judge scoring on the rewrite result — detection validity and three quality rubrics (privacy, quality, style).
Evaluation makes additional LLM calls per record. For larger datasets, evaluate
a preview first; this tutorial evaluates all 25 rows to demonstrate the complete workflow.
This holistic judge is independent of pipeline leakage scoring, so their assessments may differ.
See Evaluation for details.
evaluated = anonymizer.evaluate(result)
evaluated.display_record(0)
⏭️ Next steps¶
- 📊 Evaluation -- learn about the detection validity and rewrite quality judges in detail.
- 🔍 Inspecting Detected Entities -- debug what the detection pipeline found before rewriting.
- Try it on your own data! Swap in your CSV, define entity labels for your
domain, and set a
PrivacyGoalthat fits -- you've got all the building blocks.