Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/concepts/choosing-a-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ What to include:
- The domain (clinical, legal, financial, customer support, etc.)
- The genre (notes, transcripts, opinions, biographies)
- Anything about the source the engine couldn't infer from a single record (e.g. "transcribed phone calls — expect disfluencies")
- `data_summary` is the only way to provide a soft do-not-tag list for the augmenter when `entity_labels=None` — the augmenter is free to invent labels beyond `DEFAULT_ENTITY_LABELS`, so use it to tell the LLM what *not* to tag (e.g. "do not tag generic anatomical terms, medication class names, or job titles as PII").
- `data_summary` is a soft way to guide the augmenter when `entity_labels=None` — the augmenter is free to invent labels beyond `DEFAULT_ENTITY_LABELS`, so use it to tell the LLM what *not* to tag (e.g. "do not tag generic anatomical terms, medication class names, or job titles as PII"). For a hard exclusion of specific label types, use `Detect.entity_label_denylist` instead.

What to leave out:

- Lists of entity types **you want detected** (those go in `Detect.entity_labels`)
- Lists of entity types **you never want detected** (those go in `Detect.entity_label_denylist`)
- Privacy/utility goals (those go in `Rewrite.privacy_goal`)
- Substitute behavior instructions (e.g. "names should remain Portuguese", "preserve numeric magnitude") — those go in `Substitute(instructions)`
- Generic phrasing ("text data" adds no signal)
Expand Down Expand Up @@ -78,6 +79,18 @@ from anonymizer import DEFAULT_ENTITY_LABELS, Detect
detect = Detect(entity_labels=[*DEFAULT_ENTITY_LABELS, "clinical_facility", "diagnosis_code", "medication_name"])
```

### `entity_label_denylist`

Use when you want to **exclude** specific label types from detection without enumerating the entire allowlist. Denied labels are removed before GLiNER runs, so they are never detected, augmented, or surfaced in results. The evaluation judges also ignore denied label types so they don't lower your coverage score.

```python
# Never detect occupation or gender, keep everything else
Detect(entity_label_denylist=["occupation", "gender"])

# Combine with an explicit allowlist — denylist always wins
Detect(entity_labels=["first_name", "email", "city"], entity_label_denylist=["city"])
```

### `gliner_threshold`

Default `0.3`. The validator catches false positives downstream, so erring low is safe.
Expand Down
16 changes: 16 additions & 0 deletions docs/concepts/detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ config = AnonymizerConfig(
| Field | Default | Description |
|-------|---------|-------------|
| `entity_labels` | `None` (all defaults) | List of labels to detect. Leave unset (or pass `None`) to use the full default set. |
| `entity_label_denylist` | `None` | List of labels to **never** detect, even if present in `entity_labels` or the default set. Denied labels are excluded before GLiNER and the LLM prompts run, and are also filtered from the final entity output as a safety net. |
| `gliner_threshold` | `0.3` | GLiNER confidence threshold (0.0--1.0). Lower values detect more entities but may increase false positives. |
| `validation_max_entities_per_call` | `100` | Maximum candidate entities per validator LLM call. Rows with more candidates are split into chunks. See [Chunked validation](#chunked-validation). |
| `validation_excerpt_window_chars` | `500` | Characters of context included before and after a chunk's entity spans in the validator prompt. Bounds per-chunk prompt size; not the model's context-window limit. |
Expand Down Expand Up @@ -104,6 +105,21 @@ Detect(entity_labels=["first_name", "last_name", "email"])
# Permissive: detect all defaults + LLM can infer new label types
Detect() # entity_labels=None
```

### Excluding labels with a deny list

Use `entity_label_denylist` to exclude specific labels from detection without having to enumerate the entire allowlist. Denied labels are removed before GLiNER runs and before the LLM prompts are built, so they are never detected or augmented.

```python
# Detect all defaults except occupation and gender
Detect(entity_label_denylist=["occupation", "gender"])

# Combine with an explicit allowlist — denylist always wins
Detect(entity_labels=["first_name", "email", "city"], entity_label_denylist=["city"])
```

!!! warning
If every label in `entity_labels` is also in `entity_label_denylist`, the effective detection set is empty and no entities will be detected. Anonymizer logs a warning when this happens.
## Tuning the threshold

For `gliner_threshold`, start with the default `0.3`. If you're seeing too many false positives, raise it to `0.5`. If entities are being missed, try lowering to `0.2`. The LLM validation step catches many false positives, so erring on the side of lower thresholds is usually safe.
Expand Down
1 change: 1 addition & 0 deletions docs/concepts/evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Note: the judge measures detection recall, not output leakage. A value detected
The judge is scoped and contextualized by the same signals used during anonymization:

- **`entity_labels`** — the detection taxonomy in scope; the judge only reports values whose type falls within it.
- **`entity_label_denylist`** — labels explicitly excluded from detection; the judge ignores entities of these types so denied labels are never penalised in the coverage score.
- **`data_summary`** — used purely to interpret literal values and their semantic types, never to invent entities absent from the text.

| Output column | Type | Description |
Expand Down
10 changes: 7 additions & 3 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,13 @@ Verify by re-running `preview` with `Annotate` and confirming the entity now app

Symptoms: detected entities include obvious common words, dates that aren't dates, etc.

1. **Raise `gliner_threshold`** to `0.5`. The augmenter will pick up real misses, so this rarely costs recall.
2. **Lower `validation_excerpt_window_chars`** (default `500`) if context-driven validation is being misled by far-away sentences. Smaller per-chunk prompts trade context for precision.
3. **Sanity-check the validator with an `Annotate` preview.** A flaky validator (or a misconfigured alias) returns "keep" on almost everything, which presents as recall going way up — easiest spotted by eyeballing the entity list on a handful of rows.
1. **Use `Detect.entity_label_denylist`** if a whole label type is systematically noisy for your data (e.g. `occupation` tagging generic job words, `age` tagging durations). This is the cleanest fix — denied labels are excluded before GLiNER runs and never appear in results.
```python
Detect(entity_label_denylist=["occupation", "age"])
```
2. **Raise `gliner_threshold`** to `0.5`. The augmenter will pick up real misses, so this rarely costs recall.
3. **Lower `validation_excerpt_window_chars`** (default `500`) if context-driven validation is being misled by far-away sentences. Smaller per-chunk prompts trade context for precision.
4. **Sanity-check the validator with an `Annotate` preview.** A flaky validator (or a misconfigured alias) returns "keep" on almost everything, which presents as recall going way up — easiest spotted by eyeballing the entity list on a handful of rows.

### A new domain isn't being detected well

Expand Down
134 changes: 74 additions & 60 deletions skills/anonymizer/BENCHMARK.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,99 @@
<!-- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Skill Benchmark: anonymizer

# Evaluation Report
> ✅ **Overall verdict: PASS — Recommended for publication**

Evaluation report for the `anonymizer` skill before publication through
NVSkills-Eval.
## Publication Recommendation

This benchmark file records the publication-ready evaluation plan and task
composition for NeMo Anonymizer. The external NVSkills-Eval run has not been
executed in this local workspace, so this branch intentionally reports no
Anonymizer scores.
Recommended for publication based on the completed evaluation evidence in this report.

## Evaluation Summary
## Evaluation Metadata

- Skill: `anonymizer`
- Evaluation date: pending external `/nvskills-ci` run
- NVSkills-Eval profile: external
- Environment: external NVSkills-Eval runner
- Dataset: 6 evaluation tasks
- Attempts per task: recorded by external NVSkills-Eval after execution
- Pass threshold: recorded by external NVSkills-Eval after execution
- Overall verdict: pending external NVSkills-Eval run
- Evaluation date: 2026-08-12
- Evaluator version: `1.2.4`
- Agents: Claude Code (`aws/anthropic/bedrock-claude-opus-4-8`), Codex (`openai/openai/gpt-5.5`)
- Tasks: 6 evaluation tasks (4 positive, 2 negative)
- Dataset digest: `sha256:c2c13b2d794c6117dac0402f1261bd2d80972085c5e716426bedff6b3d59b8ae` (skill-evaluator-dataset-snapshot/1)
- Attempts per task: 1
- Environment: `k8s-sandbox`
- Tier 3 evidence: required for publication

## Agents Used
Each task attempt ran in its own isolated sandbox pod.

Agent-level measured results are pending the external NVSkills-Eval run.
## What This Report Answers

## Metrics Used
The three-tier evaluation checks whether the skill:

Reported benchmark dimensions:
- is safe to use;
- produces correct answers;
- is discovered and activated when needed;
- helps the agent complete the user's goal and expected workflow; and
- avoids wasted skill and tool usage.

- Security: checks whether skill-assisted execution avoids unsafe behavior such
as secret leakage, destructive commands, or unauthorized access.
- Correctness: checks whether the agent follows the expected workflow and
produces the correct final output.
- Discoverability: checks whether the agent loads the skill when relevant and
avoids using it when irrelevant.
- Effectiveness: checks whether the agent performs measurably better with the
skill than without it.
- Efficiency: checks whether the agent uses fewer tokens and avoids redundant
work.
## Results at a Glance

Underlying evaluation signals will be recorded from the external
NVSkills-Eval output after execution.
| Measure | Claude Code (Baseline → Skill Uplift) | Codex (Baseline → Skill Uplift) |
|---|---:|---:|
| Overall | 58% → 94% (+36 points) | 68% → 93% (+25 points) |
| Security | 100% → 92% (-8 points) | 83% → 83% (±0 points) |
| Correctness | 50% → 100% (+50 points) | 83% → 97% (+13 points) |
| Discoverability | 50% → 99% (+49 points) | 67% → 94% (+27 points) |
| Effectiveness | 52% → 89% (+38 points) | 65% → 93% (+28 points) |
| Efficiency | 38% → 89% (+51 points) | 42% → 97% (+55 points) |

## Test Tasks
**How to read this table:** baseline is the same task attempted without the target skill. Uplift is `skill score - baseline score`, shown in percentage points.

The benchmark dataset contains 6 evaluation tasks:
Example: `47% → 92% (+45 points)` means the skill-assisted run scored 92%, 45 percentage points above its 47% no-skill baseline.

- Positive tasks: 4 tasks where the skill is expected to activate.
- Negative tasks: 2 tasks where no skill is expected.
- Unlabeled tasks: 0 tasks where positive/negative intent cannot be inferred.
## Tier Status

Entries with `should_trigger: true` and `expected_skill: "anonymizer"` are
positive skill-activation cases. Entries with `should_trigger: false` and
`expected_skill: null` are negative activation cases.
| Tier | Purpose | Status | Evidence |
|---|---|---|---|
| Tier 1 | Static validation | **PASSED WITH OBSERVATIONS** | 1 validator(s); 2 finding(s) |
| Tier 2 | Semantic deduplication | **NOT RUN** | No result was recorded |
| Tier 3 | Live agent evaluation | **PASS** | 2 agent(s); 6 task(s) |

## Results
## Findings and Observations

External NVSkills-Eval execution is pending. No copied or locally inferred
Anonymizer results are reported here.
<details>
<summary>Show detailed findings and successful checks</summary>

| Dimension | Tasks | Result |
|---|---:|---|
| Security | 6 | Pending external NVSkills-Eval run |
| Correctness | 6 | Pending external NVSkills-Eval run |
| Discoverability | 6 | Pending external NVSkills-Eval run |
| Effectiveness | 6 | Pending external NVSkills-Eval run |
| Efficiency | 6 | Pending external NVSkills-Eval run |
- **MEDIUM** SCHEMA/body_recommended_section: Missing recommended section: '## Instructions' (`skills/anonymizer/SKILL.md`)
- **MEDIUM** SCHEMA/body_recommended_section: Missing recommended section: '## Examples' (`skills/anonymizer/SKILL.md`)

## Tier 1: Static Validation Summary
</details>

Local static validation is covered by this branch's validation evidence. The
external NVSkills-Eval Tier 1 result is pending the `/nvskills-ci` run.
## Scoring Methodology

## Tier 2: Deduplication Summary
<details>
<summary>Show dimension definitions, source signals, and thresholds</summary>

External NVSkills-Eval deduplication results are pending.
| Dimension | Question | Scored signals |
|---|---|---|
| Security | Is it safe to use? | `security` (100%) |
| Correctness | Is the answer correct? | `accuracy` (100%) |
| Discoverability | Was the right skill loaded when needed? | `skill_execution` (100%) |
| Effectiveness | Did the skill help complete the task? | `goal_accuracy` (50%) + `behavior_check` (50%) |
| Efficiency | Did it avoid wasted tool or skill usage? | `skill_efficiency` (100%) |

## Publication Recommendation
- Dimension bands: PASS at 50% or above; NEUTRAL from 40% to below 50%; FAIL below 40%.
- Overall Tier 3 lift: PASS at +5 points or more; FAIL at -10 points or less; values between those bands are NEUTRAL.
- Overall verdict: PASS only when every configured dimension passes for at least one supported agent. Lift is reported as diagnostic evidence and does not override this gate.
- The 50% attempt pass threshold is a separate per-task gate; it is not the dimension pass threshold.
- Effectiveness is the equal-weight mean of goal completion (`goal_accuracy`) and expected workflow adherence (`behavior_check`).
- Token efficiency is a separate report-only signal. It does not change a dimension score or the overall verdict.

Signals present in this run:

- `security` (Security): unsafe operations, secret leakage, and unauthorized access.
- `skill_execution` (Skill Execution): whether the expected skill was found and executed.
- `skill_efficiency` (Efficiency): routing quality, workspace-aware skill reads, and productive tool use.
- `accuracy` (Accuracy): final-answer correctness against the reference answer.
- `goal_accuracy` (Goal Accuracy): whether the user's goal was achieved.
- `behavior_check` (Behavior Check): whether the expected workflow behavior was followed.

</details>

## Freshness

Proceed to external NVSkills-Eval and signing. Publication should depend on the
external evaluation and signing results rather than this local preparation
branch alone.
Regenerate this benchmark when the skill, evaluation dataset, target agent/model, evaluator version, environment, or scoring policy changes.
1 change: 1 addition & 0 deletions skills/anonymizer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ regulatory and business context.
# Usage Tips and Common Pitfalls

- **`Detect.entity_labels=None` (the default) is permissive** — the augmenter LLM may invent labels not in `DEFAULT_ENTITY_LABELS`. Setting an explicit list switches to **strict mode** where *only* the listed labels are detected. To add domain labels, *extend* the default, don't replace it: `entity_labels=[*DEFAULT_ENTITY_LABELS, "clinical_facility", ...]` (`DEFAULT_ENTITY_LABELS` is a tuple, so unpack it into a list). Match the snake_case convention of `DEFAULT_ENTITY_LABELS`.
- **`Detect.entity_label_denylist`** excludes specific label types from detection entirely — denied labels are removed before GLiNER runs and are never detected, augmented, or penalised in evaluation scores. Use it when a label type is systematically noisy for your data or should never be anonymized (e.g. `Detect(entity_label_denylist=["occupation", "gender"])`). The denylist takes precedence over `entity_labels` — a label in both is never detected.
- **GLiNER is zero-shot** — entity labels are natural-language concept names (e.g. `"clinical_facility"`, `"internal_project_codename"`), not codes or enum values. Any concept you can name in English is a label GLiNER can detect.
- **`Rewrite.instructions` is a dead field today** — it exists on the model but the rewrite engine never reads it. Do not use it. Put rewriter guidance in `privacy_goal.protect` / `privacy_goal.preserve` instead.
- **`risk_tolerance` only applies to Rewrite mode**, not Replace.
Expand Down
Loading
Loading