Skip to content

feat(examples): introduce NIM-based example notebooks, retire superseded ones (NGUARD-724) - #1906

Merged
tgasser-nv merged 14 commits into
developfrom
dev/schilton/better-notebook-examples
Jun 24, 2026
Merged

feat(examples): introduce NIM-based example notebooks, retire superseded ones (NGUARD-724)#1906
tgasser-nv merged 14 commits into
developfrom
dev/schilton/better-notebook-examples

Conversation

@svenchilton

@svenchilton svenchilton commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Description

feat(examples): introduce NIM-based example notebooks, retire superseded ones (NGUARD-724)

Three single-subject example notebooks demonstrating one NeMo Guardrails rail each in a distinct real-world vertical, with rigorous evaluation against hand-curated 20-row in-repo subsets. Replaces the earlier text-based examples that are superseded by the NIM versions.

Added:

  • gliner_pii_detection_nim.ipynb -- insurance claim anonymization. Baseline on the in-repo subset: precision 0.73 / recall 0.99 / F1 0.84. Over-masking is the headline weakness; Discussion identifies three patterns (out-of-scope entity types despite the entities focus list, email-local substring double-detection, over-broad vehicle_identifier / street_address / phone_number spans).

  • content_safety_nim.ipynb -- online-gaming community moderation. Baseline:

  • topic_control_nim.ipynb -- IP-law research assistant. Baseline: precision 0.92 / recall 0.92 / F1 0.92 -- most balanced of the three. Single FN on a compound-domain prompt (patents-in-divorce); single FP on a policy-coverage gap (IPC acronym not in policy text). Discussion recommends expanding the policy with procedural acronyms (IPC, USPC, CPC, TTAB, PTAB, IPR, PGR, CBM) and key doctrines by name.

Also added: hand-curated 20-row evaluation subsets at examples/notebooks/data/ plus their reproducibility-preserving build scripts.

Removed (superseded):

  • content_safety_tutorial.ipynb -- superseded by content_safety_nim.ipynb.
  • gliner_pii_detection.ipynb -- superseded by gliner_pii_detection_nim.ipynb.

Shared eval-loop infrastructure across all three notebooks:

  • Deployment-aware throttle (THROTTLE_S = <remote_value> if DEPLOYMENT == 'remote' else 0.0; 0 for in-process NIM containers).
  • Retry-with-exponential-backoff helper catching 429 rate-limit errors (sleeps 2**attempt s for up to MAX_RETRIES attempts).
  • _Drop429Filter on nemoguardrails.rails.llm.llmrails to suppress the verbose ERROR-level tracebacks emitted before re-raise on each 429.

Deferred to follow-on commits:

  • jailbreak_detection_nim.ipynb -- pivot decision pending. The in-progress retail-banking version's eval scored 0/15 recall on banking-context attacks despite the rail catching the canonical multi-paragraph DAN prompt cleanly; next step is to choose between (a) keeping the banking vertical and pivoting the narrative to defense-in-depth, or (b) switching to a vertical where canonical jailbreaks fit the rail's training distribution.
  • combined_guardrails_nim.ipynb -- possible scope broadening to multi-rail evaluation against a hand-curated healthcare subset (scoping doc drafted, not yet committed). Current notebook left as-is in this commit.

@tgasser-nv, @Pouyanpi, and/or @miyoungc, please review.

Related Issue(s)

Checklist

  • I've read the CONTRIBUTING guidelines.
  • I've updated the documentation if applicable. [NA]
  • I've added tests if applicable. [NA]
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • New Features / Documentation
    • Added example notebooks demonstrating combined guardrails, content-safety evaluation, PII detection with masking, and topic-control capabilities for healthcare and gaming scenarios.
    • Notebooks include interactive test scenarios, evaluation frameworks, and deployment guidance for both local and remote setups.
    • Added supporting evaluation datasets for content safety, jailbreak detection, PII detection, and topic control workflows.
    • Replaced outdated PII detection notebook with updated version including comprehensive evaluation metrics.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces five new NeMo Guardrails example notebooks with local/remote NIM deployment support and four supporting Python dataset-generation scripts. Four independent notebooks showcase topic control, content safety, GLiNER PII masking, and a combined multi-rail example, each with deployment setup, smoke tests, batch evaluation, and metrics. The old GLiNER PII notebook is removed and replaced.

Changes

NeMo Guardrails NIM Example Notebooks and Evaluation Datasets

Layer / File(s) Summary
Evaluation dataset generators
examples/notebooks/data/build_content_safety_subset.py, build_jailbreak_detection_subset.py, build_pii_detection_subset.py, build_topic_control_subset.py
Four Python scripts define hard-coded test examples, write CSV evaluation datasets, and print summary statistics. These datasets are loaded by the example notebooks.
Topic control example notebook
examples/notebooks/topic_control_nim.ipynb
Demonstrates topic-control rails for an IP-law research assistant with deployment setup, YAML policy definition, smoke test, batch evaluation with remote retry/throttle, confusion-matrix metrics, per-topic recall/over-block rates, and failure analysis.
Content safety example notebook
examples/notebooks/content_safety_nim.ipynb
Demonstrates content-safety classification for online-gaming chat with 23-category NVIDIA safety taxonomy, smoke test, batch evaluation with exponential-backoff retry on 429 rate limits, and per-category confusion-matrix breakdown.
GLiNER PII masking example notebook
examples/notebooks/gliner_pii_detection_nim.ipynb
Demonstrates GLiNER-based PII masking for insurance-claim anonymization with deployment setup, smoke test, batch evaluation with async retry/backoff, entity-level span-overlap metrics, and failure analysis. Replaces the removed gliner_pii_detection.ipynb.
Combined guardrails example notebook
examples/notebooks/combined_guardrails_nim.ipynb
Showcases all four guardrails (jailbreak detection, content safety, topic control, GLiNER PII masking) in a single healthcare chatbot with six interactive test scenarios, rails.explain() inspection, and batch evaluation with refusal-string-based blocking verdict.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • NVIDIA-NeMo/Guardrails#1845: Both PRs update the GLiNER PII example notebooks to the newer NIM-oriented/local-vs-remote flow (main PR removes examples/notebooks/gliner_pii_detection.ipynb and adds gliner_pii_detection_nim.ipynb).

Suggested reviewers

  • tgasser-nv
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: introducing NIM-based example notebooks while retiring superseded ones, with direct reference to the issue (NGUARD-724).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Results For Major Changes ✅ Passed Three notebooks include baseline metrics and failure analysis. All changes are examples/documentation, not core product changes that would require testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/schilton/better-notebook-examples

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/notebooks/combined_guardrails_nim.ipynb`:
- Line 318: The notebook sets
config.rails.config.jailbreak_detection.nim_base_url to
"http://localhost:8125/v1/" which doubles the /v1 path because
nim_server_endpoint already includes "/v1/..."; change nim_base_url to the host
root (e.g., "http://localhost:8125") so that nim_server_endpoint can be appended
cleanly, and update any related local-mode construction logic that concatenates
nim_base_url + nim_server_endpoint to avoid duplicate slashes.
- Around line 605-606: The current detection sets blocked = REFUSAL in content
which treats any occurrence of the REFUSAL token as a block; change the check to
use prefix or exact equality against content (e.g., use
content.startswith(REFUSAL) or content == REFUSAL) so only responses that begin
with or exactly equal the refusal marker are classified as blocked; update the
logic that computes preview (the preview = content[:55]... line) only after
determining blocked status so previews remain meaningful for non-refusal
responses.

In `@examples/notebooks/content_safety_nim.ipynb`:
- Line 634: This file is missing a trailing newline at EOF; add a final newline
character after the last closing brace ('}') so the file ends with a newline
(fixes the end-of-file-fixer pre-commit lint failure).

In `@examples/notebooks/data/build_pii_detection_subset.py`:
- Line 293: The print for average entities per PII-bearing row can raise
ZeroDivisionError when all rows have no PII; change the logic around the
expression using entity_count, rows, and rows_no_pii to compute denom =
len(rows) - rows_no_pii and then print a safe fallback (e.g., 0 or "N/A") when
denom == 0, otherwise print entity_count / denom formatted to one decimal;
update the code that prints the result at the spot where print(f"Avg entities
per PII-bearing row: {entity_count / (len(rows) - rows_no_pii):.1f}") is
currently called.

In `@examples/notebooks/gliner_pii_detection_nim.ipynb`:
- Line 724: The notebook gliner_pii_detection_nim.ipynb is missing a final
newline so pre-commit's end-of-file-fixer changes it in CI; open the notebook
file, ensure the file ends with a single trailing newline (save so git records
the newline-normalized content), run pre-commit or git add and recommit the
updated notebook, and push the commit to clear the pipeline blocker.
- Around line 456-457: The loop currently catches a blind Exception and is
top-level; wrap the inference loop into an async function and replace the broad
except Exception with specific exception types: keep detect_with_retry as async,
move the for-loop that builds predicted_entities_per_row into async def main(),
and at the end call it with asyncio.run(main()) (or await it if the notebook
cell supports top-level await). Change the error handler in the loop from
"except Exception as e" to something like "except (ValueError, ConnectionError,
asyncio.TimeoutError) as e" (or include httpx.HTTPStatusError if your HTTP
client is httpx) so you only catch expected error classes from gliner_request
and network failures; preserve the existing per-row fallback behavior (append []
and print).

In `@examples/notebooks/topic_control_nim.ipynb`:
- Line 650: The file ends without a trailing newline (the JSON currently
terminates with a closing brace "}") which triggers the end-of-file-fixer lint;
add a single newline at the end of the notebook JSON so the file ends with "\n"
(ensure the final character is a newline) and re-save the file to unblock the
pre-commit pipeline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b4c7525a-a5bc-4a0e-8319-3c01ab25327e

📥 Commits

Reviewing files that changed from the base of the PR and between 1c8688a and 02d6f64.

⛔ Files ignored due to path filters (4)
  • examples/notebooks/data/content_safety_subset.csv is excluded by !**/*.csv
  • examples/notebooks/data/jailbreak_detection_subset.csv is excluded by !**/*.csv
  • examples/notebooks/data/pii_detection_subset.csv is excluded by !**/*.csv
  • examples/notebooks/data/topic_control_subset.csv is excluded by !**/*.csv
📒 Files selected for processing (10)
  • examples/notebooks/combined_guardrails_nim.ipynb
  • examples/notebooks/content_safety_nim.ipynb
  • examples/notebooks/content_safety_tutorial.ipynb
  • examples/notebooks/data/build_content_safety_subset.py
  • examples/notebooks/data/build_jailbreak_detection_subset.py
  • examples/notebooks/data/build_pii_detection_subset.py
  • examples/notebooks/data/build_topic_control_subset.py
  • examples/notebooks/gliner_pii_detection.ipynb
  • examples/notebooks/gliner_pii_detection_nim.ipynb
  • examples/notebooks/topic_control_nim.ipynb
💤 Files with no reviewable changes (1)
  • examples/notebooks/gliner_pii_detection.ipynb

Comment thread examples/notebooks/combined_guardrails_nim.ipynb Outdated
Comment thread examples/notebooks/combined_guardrails_nim.ipynb Outdated
Comment thread examples/notebooks/content_safety_nim.ipynb Outdated
Comment thread examples/notebooks/data/build_pii_detection_subset.py Outdated
Comment thread examples/notebooks/gliner_pii_detection_nim.ipynb Outdated
Comment thread examples/notebooks/gliner_pii_detection_nim.ipynb Outdated
Comment thread examples/notebooks/topic_control_nim.ipynb Outdated
@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces three NIM-based example notebooks (content_safety_nim, gliner_pii_detection_nim, topic_control_nim) with hand-curated 20-row evaluation subsets and shared retry/throttle infrastructure, replacing two superseded text-based notebooks; it also updates the combined-guardrails notebook with rate-limiting and the correct jailbreak-detection base URL.

  • New single-rail notebooks each follow a consistent structure: deployment-aware config setup (local NIM containers or NVIDIA-hosted endpoint), a smoke test, a full eval loop against an in-repo CSV with exponential-backoff retry on 429s, precision/recall/F1 metrics, per-category breakdowns, and a failure-analysis discussion with actionable next steps.
  • combined_guardrails_nim.ipynb now correctly uses http://localhost:8125 (no /v1/ suffix) for local jailbreak detection, adds THROTTLE_S/MAX_RETRIES/generate_with_retry, and suppresses verbose 429 tracebacks — all previously flagged issues resolved.
  • Build scripts for all four evaluation subsets are added under examples/notebooks/data/, enabling reproducible regeneration of the CSVs.

Confidence Score: 5/5

Safe to merge — the notebooks are documentation/example material with no impact on library runtime code, and all previously flagged bugs have been resolved in follow-on commits.

All prior blocking issues (incorrect jailbreak URL, dead retry code, missing rate limiting, error-row metric inflation, PII smoke-test description mismatch) have been addressed in intermediate commits. The two remaining observations are a stale inline code comment and hardcoded eval-loop parameters — neither affects correctness of the library or the notebook's core behavior.

The content_safety_nim.ipynb smoke-test code comment (line 271) and the gliner_pii_detection_nim.ipynb eval-loop threshold/model values are worth a quick pass before publishing the notebooks externally.

Important Files Changed

Filename Overview
examples/notebooks/content_safety_nim.ipynb New notebook for online-gaming content-safety evaluation with 20-row curated subset, retry/throttle infrastructure, and full metrics breakdown; one stale code comment contradicts the updated markdown description for the smoke-test benign case.
examples/notebooks/gliner_pii_detection_nim.ipynb New PII-masking notebook for insurance-claim anonymization; eval loop calls gliner_request directly with hardcoded threshold=0.5 and model values that are separate from—and can drift from—the config cell values.
examples/notebooks/topic_control_nim.ipynb New IP-law topic-control notebook with well-documented eval loop, retry/throttle pattern, and thorough failure analysis including the IPC-acronym FP and compound-domain FN.
examples/notebooks/combined_guardrails_nim.ipynb Combined-rail healthcare notebook now has rate-limiting (THROTTLE_S), retry logic (generate_with_retry), and the correct nim_base_url for local jailbreak detection; all previously flagged issues addressed.
examples/notebooks/data/build_pii_detection_subset.py Build script for the 20-row PII CSV with span-level ground truth serialized as JSON in the entities column.
examples/notebooks/data/build_content_safety_subset.py Build script for the 20-row content-safety CSV; produces balanced toxic/benign examples with NIM S-code labels.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User Prompt] --> B{Input Rail Chain}
    B --> C[Jailbreak Detection]
    C -->|blocked| BLOCK[Refusal Response]
    C -->|pass| D[Content Safety Check Input]
    D -->|blocked| BLOCK
    D -->|pass| E[Topic Control Check Input]
    E -->|blocked| BLOCK
    E -->|pass| F[GLiNER PII Detect/Mask Input]
    F -->|blocked/masked| G[Main LLM]
    G --> H{Output Rail Chain}
    H --> I[Content Safety Check Output]
    I -->|blocked| BLOCK
    I -->|pass| J[GLiNER PII Detect Output]
    J -->|blocked| BLOCK
    J -->|pass| RESP[Response to User]

    subgraph Eval Loop
        CSV[CSV Subset / Full HF Dataset] --> LOOP[For each row]
        LOOP --> RETRY[classify_with_retry / detect_with_retry]
        RETRY -->|429| BACKOFF[Exponential Backoff 1-16s]
        BACKOFF --> RETRY
        RETRY -->|success| PRED[predicted column]
        RETRY -->|all retries exhausted| NONE[None / error row]
        PRED --> METRICS[Precision / Recall / F1 + Per-Category]
        NONE --> DROPNA[dropna exclusion]
        DROPNA --> METRICS
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
examples/notebooks/content_safety_nim.ipynb:271
**Stale code comment contradicts the updated markdown description**

The inline comment on this line still reads `#   1. Benign — gaming trash-talk, should pass through`, but the smoke-test markdown cell (fixed in commit 2d5cf56) now correctly states the message is *expected to be blocked* as a headline Profanity FP. A reader scanning only the code comments receives the opposite expectation from a reader who reads the markdown. The comment should be updated to something like `#   1. Benign — gaming trash-talk, expected to be over-blocked (Profanity S12 FP)` to stay consistent.

### Issue 2 of 2
examples/notebooks/gliner_pii_detection_nim.ipynb:462-470
**Eval loop uses hardcoded params that can silently diverge from the config cell**

The `detect_with_retry` call hardcodes `threshold=0.5` and `model="nvidia/gliner-pii"` independently of the values set in the config YAML. If a reader follows the Discussion's recommendation to raise `threshold` to 0.7 (to suppress low-confidence FPs) and updates the config cell, the eval loop will continue running at 0.5 — producing results that don't match the configured rail. The smoke test uses `rails.generate()` which reads from config, so the two measurement paths would diverge silently.

Consider defining module-level constants (`GLINER_THRESHOLD = 0.5`, `GLINER_MODEL = "nvidia/gliner-pii"`) shared by both the YAML-building cell and the eval loop, or reading the values directly from `config.rails.config.gliner` after the config is built.

Reviews (15): Last reviewed commit: "fix(examples): add rate-limit handling t..." | Re-trigger Greptile

Comment thread examples/notebooks/content_safety_nim.ipynb
Comment thread examples/notebooks/content_safety_nim.ipynb
Comment thread examples/notebooks/gliner_pii_detection_nim.ipynb
@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

svenchilton added a commit that referenced this pull request May 19, 2026
CI on PR #1906 (NVIDIA-NeMo/Guardrails) caught three pre-commit failures:
- end-of-file-fixer: added trailing newlines to content_safety_nim.ipynb,
  topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb
- ruff: 7 lint errors auto-fixed across notebook code cells
- ruff-format: reformatted code cells in 4 notebooks (the three above plus
  combined_guardrails_nim.ipynb)

No semantic changes -- only whitespace, EOF newlines, and ruff-format
reformatting of code cells. Pre-commit hook now installed locally so
subsequent commits get the same treatment before push.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 19, 2026
Address Greptile feedback on PR #1906 (content_safety_nim.ipynb cell 9).
The smoke-test description previously said "git gud or get rekt scrub"
"Should pass through to the LLM and produce a normal response," but the
saved cell output shows it blocked on Profanity. Readers following the
notebook would have assumed their setup was broken when the prompt got
blocked.

Rewrite the cell to:
- Explicitly frame the benign banter as an expected FP under the current
  v3 rail calibration (over-blocks gaming vocabulary on S12 Profanity)
- Replace the misleading "If the benign message gets blocked, the rail is
  over-aggressive" debug note with a sentence confirming the over-block is
  the headline FP the eval section quantifies, not a setup problem

No code changes; markdown-only edit to cell 9.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 19, 2026
Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cell 14).
The detect_with_retry helper caught only ValueError for 429 rate-limit errors,
while the sibling functions in content_safety_nim.ipynb and topic_control_nim.ipynb
catch the broader Exception. If gliner_request's 429 path ever surfaces as a
different exception class (e.g., httpx.HTTPStatusError, RuntimeError), the
backoff logic would silently bypass and the exception would propagate immediately.

Widen the catch to Exception to match the siblings. Behavior is identical for the
current ValueError-raises-on-429 case (the `"429" in str(e)` check guards against
retrying non-429 errors). Docstring updated to document the design choice.

No code-path change in the current nemoguardrails release; future-proofs against
exception-class drift and removes the cross-notebook inconsistency.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 19, 2026
Address Greptile feedback on PR #1906 (cells 13-14 of content_safety_nim.ipynb,
topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb).

Two issues, same pattern in all three notebooks:

1. The classify_with_retry / detect_with_retry helpers had an unreachable
   post-loop return statement (return None or return []). The for-loop body
   either returns from the try branch on success, or re-raises from the except
   branch on the final attempt (attempt == MAX_RETRIES - 1). The loop never
   exits via natural termination, so the post-loop return was dead code.

2. The docstring + cell-13 markdown explainer described the backoff sleep
   sequence as "(1, 2, 4, 8, 16, 32 s)" but the actual sequence is only
   (1, 2, 4, 8, 16) — the sixth attempt raises before sleeping, so the
   2**5 = 32 s sleep never occurs.

Fixes:
- Remove the unreachable returns
- Update sleep sequence to (1, 2, 4, 8, 16) in docstrings, the gliner inline
  comment, and the cell-13 markdown explainers
- Add a sentence in each docstring clarifying that the function exits via
  try-return or except-raise — there is no post-loop return path

No code-path change; behavior was already correct, only the documentation
and dead-code clarity needed fixing.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread examples/notebooks/combined_guardrails_nim.ipynb
svenchilton added a commit that referenced this pull request May 19, 2026
… notebook

Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb).

The local-deployment override set `nim_base_url = "http://localhost:8125/v1/"`,
which collides with `nim_server_endpoint = "/v1/security/nvidia/nemoguard-jailbreak-detect"`
(set in the YAML and not overridden). The `join_nim_url` helper in
`nemoguardrails/library/jailbreak_detection/request.py` rstrips the base's
trailing slash, lstrips the endpoint's leading slash, and urljoins the two
relatively — producing `http://localhost:8125/v1/v1/security/.../nemoguard-jailbreak-detect`
(double `/v1/`, 404 from the NIM). Local jailbreak detection was silently
bypassed.

Fix: drop the `/v1/` suffix from the local base, matching the remote pattern
(`https://ai.api.nvidia.com`) and the canonical NemoGuard NIM examples in
the repo docs (caching/model-memory-cache.md, the tracing tutorial). The
`/v1` already lives in nim_server_endpoint; both deployments now follow the
same convention.

The cell 7 markdown is updated with a "Note on the jailbreak-detection NIM
URL convention" paragraph explaining the rule and the join_nim_url mechanism
so future readers don't trip on this.

Note: this is a pre-existing bug on `develop` (combined_guardrails_nim.ipynb
was inherited unchanged for PR #1906). The same incorrect pattern also lives
in jailbreak_detection_nim.ipynb on dev/schilton/update-docs-and-tutorials
and will need the same fix when that notebook ships.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 19, 2026
Address CodeRabbit feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23,
the batch evaluation cell).

The previous classifier used `blocked = REFUSAL in content`, which substring-
matches the refusal phrase anywhere in the response. An LLM response that
quotes the refusal phrase in a longer answer (e.g., "If the bot would say
'I'm sorry, I can't respond to that.' here, then ...") would be incorrectly
flagged as blocked.

Switch to a prefix match against the leading refusal token:

  REFUSAL_PREFIX = "I'm sorry, I can't respond to that"
  blocked = content.strip().startswith(REFUSAL_PREFIX)

Matches the `is_blocked` pattern already in use in the sibling single-subject
notebooks (content_safety_nim, topic_control_nim, gliner_pii_detection_nim).
Dropping the trailing period from the constant keeps the prefix match robust
to small variants in the refusal text.

CodeRabbit's secondary point about reordering the preview computation after
blocked is a non-issue — the cell already computes blocked before preview.
No change needed there.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 19, 2026
Address CodeRabbit feedback on PR #1906
(examples/notebooks/data/build_pii_detection_subset.py around line 293).

The "Avg entities per PII-bearing row" stat computed
`entity_count / (len(rows) - rows_no_pii)`, which would raise ZeroDivisionError
if a user reused the script to build a subset where every row is an FP-test
row (no PII). The current 20-row subset has 17 PII / 3 no-PII rows so this
never trips in practice, but the build script is meant to be reusable.

Fix: extract `rows_with_pii = len(rows) - rows_no_pii`, guard with
`if rows_with_pii:`, and print "N/A (no PII-bearing rows in subset)" in the
fallback case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread examples/notebooks/gliner_pii_detection_nim.ipynb
svenchilton added a commit that referenced this pull request May 20, 2026
…traces

Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cells 9-10).

Two issues:

1. The PII-free smoke-test description said the narrative "Should pass through
   to the LLM essentially unchanged," but the saved output shows [VEHICLE_IDENTIFIER]
   replacing "2018 Subaru Outback" — the GLiNER NIM's over-broad vehicle_identifier
   detection. Updated the description to acknowledge this as a known over-masking
   pattern (not a setup problem), matching the content_safety notebook's approach
   of framing expected FPs explicitly in the smoke-test section.

2. rails.explain() was called once after the for-loop, so only the last call's
   trace (PII-rich) was captured. The PII-free trace — which would show the
   [VEHICLE_IDENTIFIER] replacement — was lost. Moved rails.explain() inside
   the loop so each call gets its own trace printed inline, matching the sibling
   notebooks' smoke-test structure.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread examples/notebooks/gliner_pii_detection_nim.ipynb
svenchilton added a commit that referenced this pull request May 20, 2026
…ings

Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb
cells 14, 16, 18).

On exception, the eval loop appended [] (empty entity list) rather than
None. The metrics cell iterated over all rows without filtering, so every
gold entity in an errored row was counted as a false negative — silently
deflating recall if a 429 survived all retries. The sibling notebooks
(content_safety_nim, topic_control_nim) append None and use
`valid = df.dropna(subset=["predicted"])` to exclude errored rows.

Fixes:
- Cell 14: append None (not []) on error; use `is not None` for the
  classified-row count so PII-free rows with valid empty predictions
  aren't conflated with errors
- Cell 16: add `valid = df.dropna(subset=["predicted_entities"])` and
  iterate over `valid` instead of `df`
- Cell 18: iterate over `valid` (from cell 16) instead of `df` to avoid
  TypeError on None rows and consistent error-row exclusion

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread examples/notebooks/combined_guardrails_nim.ipynb
svenchilton added a commit that referenced this pull request May 20, 2026
Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23).

The batch evaluation loop ran 6 test cases back-to-back with no throttle,
no retry, and no exception handling. Each scenario triggers ~7 sequential
API calls (jailbreak + content safety + topic control + PII input + main
LLM + content safety output + PII output), for ~42 total calls in rapid
succession — reliably exceeding the hosted endpoint's per-minute rate limit.

Add the same three-mechanism robustness pattern used in the single-rail
notebooks:
- Deployment-aware throttle (THROTTLE_S = 0.5 for remote, slightly higher
  than the single-rail 0.3 to account for more calls per scenario)
- Retry-with-exponential-backoff helper (generate_with_retry)
- _Drop429Filter on nemoguardrails.rails.llm.llmrails to suppress verbose
  tracebacks
- Per-scenario try/except so a single failure doesn't crash the batch

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
CI on PR #1906 (NVIDIA-NeMo/Guardrails) caught three pre-commit failures:
- end-of-file-fixer: added trailing newlines to content_safety_nim.ipynb,
  topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb
- ruff: 7 lint errors auto-fixed across notebook code cells
- ruff-format: reformatted code cells in 4 notebooks (the three above plus
  combined_guardrails_nim.ipynb)

No semantic changes -- only whitespace, EOF newlines, and ruff-format
reformatting of code cells. Pre-commit hook now installed locally so
subsequent commits get the same treatment before push.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
Address Greptile feedback on PR #1906 (content_safety_nim.ipynb cell 9).
The smoke-test description previously said "git gud or get rekt scrub"
"Should pass through to the LLM and produce a normal response," but the
saved cell output shows it blocked on Profanity. Readers following the
notebook would have assumed their setup was broken when the prompt got
blocked.

Rewrite the cell to:
- Explicitly frame the benign banter as an expected FP under the current
  v3 rail calibration (over-blocks gaming vocabulary on S12 Profanity)
- Replace the misleading "If the benign message gets blocked, the rail is
  over-aggressive" debug note with a sentence confirming the over-block is
  the headline FP the eval section quantifies, not a setup problem

No code changes; markdown-only edit to cell 9.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cell 14).
The detect_with_retry helper caught only ValueError for 429 rate-limit errors,
while the sibling functions in content_safety_nim.ipynb and topic_control_nim.ipynb
catch the broader Exception. If gliner_request's 429 path ever surfaces as a
different exception class (e.g., httpx.HTTPStatusError, RuntimeError), the
backoff logic would silently bypass and the exception would propagate immediately.

Widen the catch to Exception to match the siblings. Behavior is identical for the
current ValueError-raises-on-429 case (the `"429" in str(e)` check guards against
retrying non-429 errors). Docstring updated to document the design choice.

No code-path change in the current nemoguardrails release; future-proofs against
exception-class drift and removes the cross-notebook inconsistency.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
Address Greptile feedback on PR #1906 (cells 13-14 of content_safety_nim.ipynb,
topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb).

Two issues, same pattern in all three notebooks:

1. The classify_with_retry / detect_with_retry helpers had an unreachable
   post-loop return statement (return None or return []). The for-loop body
   either returns from the try branch on success, or re-raises from the except
   branch on the final attempt (attempt == MAX_RETRIES - 1). The loop never
   exits via natural termination, so the post-loop return was dead code.

2. The docstring + cell-13 markdown explainer described the backoff sleep
   sequence as "(1, 2, 4, 8, 16, 32 s)" but the actual sequence is only
   (1, 2, 4, 8, 16) — the sixth attempt raises before sleeping, so the
   2**5 = 32 s sleep never occurs.

Fixes:
- Remove the unreachable returns
- Update sleep sequence to (1, 2, 4, 8, 16) in docstrings, the gliner inline
  comment, and the cell-13 markdown explainers
- Add a sentence in each docstring clarifying that the function exits via
  try-return or except-raise — there is no post-loop return path

No code-path change; behavior was already correct, only the documentation
and dead-code clarity needed fixing.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
… notebook

Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb).

The local-deployment override set `nim_base_url = "http://localhost:8125/v1/"`,
which collides with `nim_server_endpoint = "/v1/security/nvidia/nemoguard-jailbreak-detect"`
(set in the YAML and not overridden). The `join_nim_url` helper in
`nemoguardrails/library/jailbreak_detection/request.py` rstrips the base's
trailing slash, lstrips the endpoint's leading slash, and urljoins the two
relatively — producing `http://localhost:8125/v1/v1/security/.../nemoguard-jailbreak-detect`
(double `/v1/`, 404 from the NIM). Local jailbreak detection was silently
bypassed.

Fix: drop the `/v1/` suffix from the local base, matching the remote pattern
(`https://ai.api.nvidia.com`) and the canonical NemoGuard NIM examples in
the repo docs (caching/model-memory-cache.md, the tracing tutorial). The
`/v1` already lives in nim_server_endpoint; both deployments now follow the
same convention.

The cell 7 markdown is updated with a "Note on the jailbreak-detection NIM
URL convention" paragraph explaining the rule and the join_nim_url mechanism
so future readers don't trip on this.

Note: this is a pre-existing bug on `develop` (combined_guardrails_nim.ipynb
was inherited unchanged for PR #1906). The same incorrect pattern also lives
in jailbreak_detection_nim.ipynb on dev/schilton/update-docs-and-tutorials
and will need the same fix when that notebook ships.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
Address CodeRabbit feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23,
the batch evaluation cell).

The previous classifier used `blocked = REFUSAL in content`, which substring-
matches the refusal phrase anywhere in the response. An LLM response that
quotes the refusal phrase in a longer answer (e.g., "If the bot would say
'I'm sorry, I can't respond to that.' here, then ...") would be incorrectly
flagged as blocked.

Switch to a prefix match against the leading refusal token:

  REFUSAL_PREFIX = "I'm sorry, I can't respond to that"
  blocked = content.strip().startswith(REFUSAL_PREFIX)

Matches the `is_blocked` pattern already in use in the sibling single-subject
notebooks (content_safety_nim, topic_control_nim, gliner_pii_detection_nim).
Dropping the trailing period from the constant keeps the prefix match robust
to small variants in the refusal text.

CodeRabbit's secondary point about reordering the preview computation after
blocked is a non-issue — the cell already computes blocked before preview.
No change needed there.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request May 21, 2026
Address CodeRabbit feedback on PR #1906
(examples/notebooks/data/build_pii_detection_subset.py around line 293).

The "Avg entities per PII-bearing row" stat computed
`entity_count / (len(rows) - rows_no_pii)`, which would raise ZeroDivisionError
if a user reused the script to build a subset where every row is an FP-test
row (no PII). The current 20-row subset has 17 PII / 3 no-PII rows so this
never trips in practice, but the build script is meant to be reusable.

Fix: extract `rows_with_pii = len(rows) - rows_no_pii`, guard with
`if rows_with_pii:`, and print "N/A (no PII-bearing rows in subset)" in the
fallback case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@svenchilton
svenchilton force-pushed the dev/schilton/better-notebook-examples branch from c26eccd to f903854 Compare May 21, 2026 16:44
svenchilton added a commit that referenced this pull request Jun 3, 2026
CI on PR #1906 (NVIDIA-NeMo/Guardrails) caught three pre-commit failures:
- end-of-file-fixer: added trailing newlines to content_safety_nim.ipynb,
  topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb
- ruff: 7 lint errors auto-fixed across notebook code cells
- ruff-format: reformatted code cells in 4 notebooks (the three above plus
  combined_guardrails_nim.ipynb)

No semantic changes -- only whitespace, EOF newlines, and ruff-format
reformatting of code cells. Pre-commit hook now installed locally so
subsequent commits get the same treatment before push.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address Greptile feedback on PR #1906 (content_safety_nim.ipynb cell 9).
The smoke-test description previously said "git gud or get rekt scrub"
"Should pass through to the LLM and produce a normal response," but the
saved cell output shows it blocked on Profanity. Readers following the
notebook would have assumed their setup was broken when the prompt got
blocked.

Rewrite the cell to:
- Explicitly frame the benign banter as an expected FP under the current
  v3 rail calibration (over-blocks gaming vocabulary on S12 Profanity)
- Replace the misleading "If the benign message gets blocked, the rail is
  over-aggressive" debug note with a sentence confirming the over-block is
  the headline FP the eval section quantifies, not a setup problem

No code changes; markdown-only edit to cell 9.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cell 14).
The detect_with_retry helper caught only ValueError for 429 rate-limit errors,
while the sibling functions in content_safety_nim.ipynb and topic_control_nim.ipynb
catch the broader Exception. If gliner_request's 429 path ever surfaces as a
different exception class (e.g., httpx.HTTPStatusError, RuntimeError), the
backoff logic would silently bypass and the exception would propagate immediately.

Widen the catch to Exception to match the siblings. Behavior is identical for the
current ValueError-raises-on-429 case (the `"429" in str(e)` check guards against
retrying non-429 errors). Docstring updated to document the design choice.

No code-path change in the current nemoguardrails release; future-proofs against
exception-class drift and removes the cross-notebook inconsistency.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address Greptile feedback on PR #1906 (cells 13-14 of content_safety_nim.ipynb,
topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb).

Two issues, same pattern in all three notebooks:

1. The classify_with_retry / detect_with_retry helpers had an unreachable
   post-loop return statement (return None or return []). The for-loop body
   either returns from the try branch on success, or re-raises from the except
   branch on the final attempt (attempt == MAX_RETRIES - 1). The loop never
   exits via natural termination, so the post-loop return was dead code.

2. The docstring + cell-13 markdown explainer described the backoff sleep
   sequence as "(1, 2, 4, 8, 16, 32 s)" but the actual sequence is only
   (1, 2, 4, 8, 16) — the sixth attempt raises before sleeping, so the
   2**5 = 32 s sleep never occurs.

Fixes:
- Remove the unreachable returns
- Update sleep sequence to (1, 2, 4, 8, 16) in docstrings, the gliner inline
  comment, and the cell-13 markdown explainers
- Add a sentence in each docstring clarifying that the function exits via
  try-return or except-raise — there is no post-loop return path

No code-path change; behavior was already correct, only the documentation
and dead-code clarity needed fixing.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@svenchilton
svenchilton force-pushed the dev/schilton/better-notebook-examples branch from 9c98872 to 8187c71 Compare June 3, 2026 16:23
svenchilton added a commit that referenced this pull request Jun 3, 2026
… notebook

Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb).

The local-deployment override set `nim_base_url = "http://localhost:8125/v1/"`,
which collides with `nim_server_endpoint = "/v1/security/nvidia/nemoguard-jailbreak-detect"`
(set in the YAML and not overridden). The `join_nim_url` helper in
`nemoguardrails/library/jailbreak_detection/request.py` rstrips the base's
trailing slash, lstrips the endpoint's leading slash, and urljoins the two
relatively — producing `http://localhost:8125/v1/v1/security/.../nemoguard-jailbreak-detect`
(double `/v1/`, 404 from the NIM). Local jailbreak detection was silently
bypassed.

Fix: drop the `/v1/` suffix from the local base, matching the remote pattern
(`https://ai.api.nvidia.com`) and the canonical NemoGuard NIM examples in
the repo docs (caching/model-memory-cache.md, the tracing tutorial). The
`/v1` already lives in nim_server_endpoint; both deployments now follow the
same convention.

The cell 7 markdown is updated with a "Note on the jailbreak-detection NIM
URL convention" paragraph explaining the rule and the join_nim_url mechanism
so future readers don't trip on this.

Note: this is a pre-existing bug on `develop` (combined_guardrails_nim.ipynb
was inherited unchanged for PR #1906). The same incorrect pattern also lives
in jailbreak_detection_nim.ipynb on dev/schilton/update-docs-and-tutorials
and will need the same fix when that notebook ships.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address CodeRabbit feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23,
the batch evaluation cell).

The previous classifier used `blocked = REFUSAL in content`, which substring-
matches the refusal phrase anywhere in the response. An LLM response that
quotes the refusal phrase in a longer answer (e.g., "If the bot would say
'I'm sorry, I can't respond to that.' here, then ...") would be incorrectly
flagged as blocked.

Switch to a prefix match against the leading refusal token:

  REFUSAL_PREFIX = "I'm sorry, I can't respond to that"
  blocked = content.strip().startswith(REFUSAL_PREFIX)

Matches the `is_blocked` pattern already in use in the sibling single-subject
notebooks (content_safety_nim, topic_control_nim, gliner_pii_detection_nim).
Dropping the trailing period from the constant keeps the prefix match robust
to small variants in the refusal text.

CodeRabbit's secondary point about reordering the preview computation after
blocked is a non-issue — the cell already computes blocked before preview.
No change needed there.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address CodeRabbit feedback on PR #1906
(examples/notebooks/data/build_pii_detection_subset.py around line 293).

The "Avg entities per PII-bearing row" stat computed
`entity_count / (len(rows) - rows_no_pii)`, which would raise ZeroDivisionError
if a user reused the script to build a subset where every row is an FP-test
row (no PII). The current 20-row subset has 17 PII / 3 no-PII rows so this
never trips in practice, but the build script is meant to be reusable.

Fix: extract `rows_with_pii = len(rows) - rows_no_pii`, guard with
`if rows_with_pii:`, and print "N/A (no PII-bearing rows in subset)" in the
fallback case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
…traces

Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cells 9-10).

Two issues:

1. The PII-free smoke-test description said the narrative "Should pass through
   to the LLM essentially unchanged," but the saved output shows [VEHICLE_IDENTIFIER]
   replacing "2018 Subaru Outback" — the GLiNER NIM's over-broad vehicle_identifier
   detection. Updated the description to acknowledge this as a known over-masking
   pattern (not a setup problem), matching the content_safety notebook's approach
   of framing expected FPs explicitly in the smoke-test section.

2. rails.explain() was called once after the for-loop, so only the last call's
   trace (PII-rich) was captured. The PII-free trace — which would show the
   [VEHICLE_IDENTIFIER] replacement — was lost. Moved rails.explain() inside
   the loop so each call gets its own trace printed inline, matching the sibling
   notebooks' smoke-test structure.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
…ings

Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb
cells 14, 16, 18).

On exception, the eval loop appended [] (empty entity list) rather than
None. The metrics cell iterated over all rows without filtering, so every
gold entity in an errored row was counted as a false negative — silently
deflating recall if a 429 survived all retries. The sibling notebooks
(content_safety_nim, topic_control_nim) append None and use
`valid = df.dropna(subset=["predicted"])` to exclude errored rows.

Fixes:
- Cell 14: append None (not []) on error; use `is not None` for the
  classified-row count so PII-free rows with valid empty predictions
  aren't conflated with errors
- Cell 16: add `valid = df.dropna(subset=["predicted_entities"])` and
  iterate over `valid` instead of `df`
- Cell 18: iterate over `valid` (from cell 16) instead of `df` to avoid
  TypeError on None rows and consistent error-row exclusion

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23).

The batch evaluation loop ran 6 test cases back-to-back with no throttle,
no retry, and no exception handling. Each scenario triggers ~7 sequential
API calls (jailbreak + content safety + topic control + PII input + main
LLM + content safety output + PII output), for ~42 total calls in rapid
succession — reliably exceeding the hosted endpoint's per-minute rate limit.

Add the same three-mechanism robustness pattern used in the single-rail
notebooks:
- Deployment-aware throttle (THROTTLE_S = 0.5 for remote, slightly higher
  than the single-rail 0.3 to account for more calls per scenario)
- Retry-with-exponential-backoff helper (generate_with_retry)
- _Drop429Filter on nemoguardrails.rails.llm.llmrails to suppress verbose
  tracebacks
- Per-scenario try/except so a single failure doesn't crash the batch

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
CI on PR #1906 (NVIDIA-NeMo/Guardrails) caught three pre-commit failures:
- end-of-file-fixer: added trailing newlines to content_safety_nim.ipynb,
  topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb
- ruff: 7 lint errors auto-fixed across notebook code cells
- ruff-format: reformatted code cells in 4 notebooks (the three above plus
  combined_guardrails_nim.ipynb)

No semantic changes -- only whitespace, EOF newlines, and ruff-format
reformatting of code cells. Pre-commit hook now installed locally so
subsequent commits get the same treatment before push.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
svenchilton added a commit that referenced this pull request Jun 3, 2026
Address Greptile feedback on PR #1906 (content_safety_nim.ipynb cell 9).
The smoke-test description previously said "git gud or get rekt scrub"
"Should pass through to the LLM and produce a normal response," but the
saved cell output shows it blocked on Profanity. Readers following the
notebook would have assumed their setup was broken when the prompt got
blocked.

Rewrite the cell to:
- Explicitly frame the benign banter as an expected FP under the current
  v3 rail calibration (over-blocks gaming vocabulary on S12 Profanity)
- Replace the misleading "If the benign message gets blocked, the rail is
  over-aggressive" debug note with a sentence confirming the over-block is
  the headline FP the eval section quantifies, not a setup problem

No code changes; markdown-only edit to cell 9.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@tgasser-nv tgasser-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a few items to clean up before merging:

  1. The gliner_pii_detection_nim.ipynb notebook has 4 cells unexecuted: "Smoke Test", "Running the Rail", "Computing Metrics", and "Failure Analysis". Could you re-run these so the cell output is filled out?
  2. The content_safety_nim.ipynb, topic_control_nim.ipynb, and combined_guardrails_nim.ipynb all docker run instructions with -e NGC_API_KEY , but no export NGC_API_KEY="..." in a previous instruction. The gliner_pii_detection_nim.ipynb notebook does include this at the top of the "Local Deployment" section.
  3. In content_safety_nim.ipynb Under Smoke Test the gaming trash-talk example should pass through according to the comment at the top of the cell. But the content-safety rail blocked it for profanity.
  4. Could you run all cells in combined_guardrails_nim.ipynb? Only the first two were run as-is.
  5. Are the jailbreak-related scripts used in this PR? Recommend removing the build_jailbreak_detection_subset.py and jailbreak_detection_subset.csv files and adding them in the jailbreak-specific PR.

svenchilton and others added 14 commits June 24, 2026 10:58
…ded ones (NGUARD-724)

Three single-subject example notebooks demonstrating one NeMo Guardrails rail each
in a distinct real-world vertical, with rigorous evaluation against hand-curated
20-row in-repo subsets. Replaces the earlier text-based examples that are
superseded by the NIM versions.

Added:

- gliner_pii_detection_nim.ipynb -- insurance claim anonymization. Baseline on
  the in-repo subset: precision 0.73 / recall 0.99 / F1 0.84. Over-masking is
  the headline weakness; Discussion identifies three patterns (out-of-scope
  entity types despite the `entities` focus list, email-local substring
  double-detection, over-broad `vehicle_identifier` / `street_address` /
  `phone_number` spans).

- content_safety_nim.ipynb -- online-gaming community moderation. Baseline:

- topic_control_nim.ipynb -- IP-law research assistant. Baseline: precision 0.92
  / recall 0.92 / F1 0.92 -- most balanced of the three. Single FN on a
  compound-domain prompt (patents-in-divorce); single FP on a policy-coverage
  gap (IPC acronym not in policy text). Discussion recommends expanding the
  policy with procedural acronyms (IPC, USPC, CPC, TTAB, PTAB, IPR, PGR, CBM)
  and key doctrines by name.

Also added: hand-curated 20-row evaluation subsets at examples/notebooks/data/
plus their reproducibility-preserving build scripts.

Removed (superseded):

- content_safety_tutorial.ipynb -- superseded by content_safety_nim.ipynb.
- gliner_pii_detection.ipynb -- superseded by gliner_pii_detection_nim.ipynb.

Shared eval-loop infrastructure across all three notebooks:

- Deployment-aware throttle (THROTTLE_S = <remote_value> if DEPLOYMENT == 'remote'
  else 0.0; 0 for in-process NIM containers).
- Retry-with-exponential-backoff helper catching 429 rate-limit errors (sleeps
  2**attempt s for up to MAX_RETRIES attempts).
- _Drop429Filter on nemoguardrails.rails.llm.llmrails to suppress the verbose
  ERROR-level tracebacks emitted before re-raise on each 429.

Deferred to follow-on commits:

- jailbreak_detection_nim.ipynb -- pivot decision pending. The in-progress
  retail-banking version's eval scored 0/15 recall on banking-context attacks
  despite the rail catching the canonical multi-paragraph DAN prompt cleanly;
  next step is to choose between (a) keeping the banking vertical and pivoting
  the narrative to defense-in-depth, or (b) switching to a vertical where
  canonical jailbreaks fit the rail's training distribution.
- combined_guardrails_nim.ipynb -- possible scope broadening to multi-rail
  evaluation against a hand-curated healthcare subset (scoping doc drafted,
  not yet committed). Current notebook left as-is in this commit.
CI on PR #1906 (NVIDIA-NeMo/Guardrails) caught three pre-commit failures:
- end-of-file-fixer: added trailing newlines to content_safety_nim.ipynb,
  topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb
- ruff: 7 lint errors auto-fixed across notebook code cells
- ruff-format: reformatted code cells in 4 notebooks (the three above plus
  combined_guardrails_nim.ipynb)

No semantic changes -- only whitespace, EOF newlines, and ruff-format
reformatting of code cells. Pre-commit hook now installed locally so
subsequent commits get the same treatment before push.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Greptile feedback on PR #1906 (content_safety_nim.ipynb cell 9).
The smoke-test description previously said "git gud or get rekt scrub"
"Should pass through to the LLM and produce a normal response," but the
saved cell output shows it blocked on Profanity. Readers following the
notebook would have assumed their setup was broken when the prompt got
blocked.

Rewrite the cell to:
- Explicitly frame the benign banter as an expected FP under the current
  v3 rail calibration (over-blocks gaming vocabulary on S12 Profanity)
- Replace the misleading "If the benign message gets blocked, the rail is
  over-aggressive" debug note with a sentence confirming the over-block is
  the headline FP the eval section quantifies, not a setup problem

No code changes; markdown-only edit to cell 9.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cell 14).
The detect_with_retry helper caught only ValueError for 429 rate-limit errors,
while the sibling functions in content_safety_nim.ipynb and topic_control_nim.ipynb
catch the broader Exception. If gliner_request's 429 path ever surfaces as a
different exception class (e.g., httpx.HTTPStatusError, RuntimeError), the
backoff logic would silently bypass and the exception would propagate immediately.

Widen the catch to Exception to match the siblings. Behavior is identical for the
current ValueError-raises-on-429 case (the `"429" in str(e)` check guards against
retrying non-429 errors). Docstring updated to document the design choice.

No code-path change in the current nemoguardrails release; future-proofs against
exception-class drift and removes the cross-notebook inconsistency.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Greptile feedback on PR #1906 (cells 13-14 of content_safety_nim.ipynb,
topic_control_nim.ipynb, gliner_pii_detection_nim.ipynb).

Two issues, same pattern in all three notebooks:

1. The classify_with_retry / detect_with_retry helpers had an unreachable
   post-loop return statement (return None or return []). The for-loop body
   either returns from the try branch on success, or re-raises from the except
   branch on the final attempt (attempt == MAX_RETRIES - 1). The loop never
   exits via natural termination, so the post-loop return was dead code.

2. The docstring + cell-13 markdown explainer described the backoff sleep
   sequence as "(1, 2, 4, 8, 16, 32 s)" but the actual sequence is only
   (1, 2, 4, 8, 16) — the sixth attempt raises before sleeping, so the
   2**5 = 32 s sleep never occurs.

Fixes:
- Remove the unreachable returns
- Update sleep sequence to (1, 2, 4, 8, 16) in docstrings, the gliner inline
  comment, and the cell-13 markdown explainers
- Add a sentence in each docstring clarifying that the function exits via
  try-return or except-raise — there is no post-loop return path

No code-path change; behavior was already correct, only the documentation
and dead-code clarity needed fixing.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… notebook

Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb).

The local-deployment override set `nim_base_url = "http://localhost:8125/v1/"`,
which collides with `nim_server_endpoint = "/v1/security/nvidia/nemoguard-jailbreak-detect"`
(set in the YAML and not overridden). The `join_nim_url` helper in
`nemoguardrails/library/jailbreak_detection/request.py` rstrips the base's
trailing slash, lstrips the endpoint's leading slash, and urljoins the two
relatively — producing `http://localhost:8125/v1/v1/security/.../nemoguard-jailbreak-detect`
(double `/v1/`, 404 from the NIM). Local jailbreak detection was silently
bypassed.

Fix: drop the `/v1/` suffix from the local base, matching the remote pattern
(`https://ai.api.nvidia.com`) and the canonical NemoGuard NIM examples in
the repo docs (caching/model-memory-cache.md, the tracing tutorial). The
`/v1` already lives in nim_server_endpoint; both deployments now follow the
same convention.

The cell 7 markdown is updated with a "Note on the jailbreak-detection NIM
URL convention" paragraph explaining the rule and the join_nim_url mechanism
so future readers don't trip on this.

Note: this is a pre-existing bug on `develop` (combined_guardrails_nim.ipynb
was inherited unchanged for PR #1906). The same incorrect pattern also lives
in jailbreak_detection_nim.ipynb on dev/schilton/update-docs-and-tutorials
and will need the same fix when that notebook ships.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address CodeRabbit feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23,
the batch evaluation cell).

The previous classifier used `blocked = REFUSAL in content`, which substring-
matches the refusal phrase anywhere in the response. An LLM response that
quotes the refusal phrase in a longer answer (e.g., "If the bot would say
'I'm sorry, I can't respond to that.' here, then ...") would be incorrectly
flagged as blocked.

Switch to a prefix match against the leading refusal token:

  REFUSAL_PREFIX = "I'm sorry, I can't respond to that"
  blocked = content.strip().startswith(REFUSAL_PREFIX)

Matches the `is_blocked` pattern already in use in the sibling single-subject
notebooks (content_safety_nim, topic_control_nim, gliner_pii_detection_nim).
Dropping the trailing period from the constant keeps the prefix match robust
to small variants in the refusal text.

CodeRabbit's secondary point about reordering the preview computation after
blocked is a non-issue — the cell already computes blocked before preview.
No change needed there.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address CodeRabbit feedback on PR #1906
(examples/notebooks/data/build_pii_detection_subset.py around line 293).

The "Avg entities per PII-bearing row" stat computed
`entity_count / (len(rows) - rows_no_pii)`, which would raise ZeroDivisionError
if a user reused the script to build a subset where every row is an FP-test
row (no PII). The current 20-row subset has 17 PII / 3 no-PII rows so this
never trips in practice, but the build script is meant to be reusable.

Fix: extract `rows_with_pii = len(rows) - rows_no_pii`, guard with
`if rows_with_pii:`, and print "N/A (no PII-bearing rows in subset)" in the
fallback case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…traces

Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb cells 9-10).

Two issues:

1. The PII-free smoke-test description said the narrative "Should pass through
   to the LLM essentially unchanged," but the saved output shows [VEHICLE_IDENTIFIER]
   replacing "2018 Subaru Outback" — the GLiNER NIM's over-broad vehicle_identifier
   detection. Updated the description to acknowledge this as a known over-masking
   pattern (not a setup problem), matching the content_safety notebook's approach
   of framing expected FPs explicitly in the smoke-test section.

2. rails.explain() was called once after the for-loop, so only the last call's
   trace (PII-rich) was captured. The PII-free trace — which would show the
   [VEHICLE_IDENTIFIER] replacement — was lost. Moved rails.explain() inside
   the loop so each call gets its own trace printed inline, matching the sibling
   notebooks' smoke-test structure.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ings

Address Greptile feedback on PR #1906 (gliner_pii_detection_nim.ipynb
cells 14, 16, 18).

On exception, the eval loop appended [] (empty entity list) rather than
None. The metrics cell iterated over all rows without filtering, so every
gold entity in an errored row was counted as a false negative — silently
deflating recall if a 429 survived all retries. The sibling notebooks
(content_safety_nim, topic_control_nim) append None and use
`valid = df.dropna(subset=["predicted"])` to exclude errored rows.

Fixes:
- Cell 14: append None (not []) on error; use `is not None` for the
  classified-row count so PII-free rows with valid empty predictions
  aren't conflated with errors
- Cell 16: add `valid = df.dropna(subset=["predicted_entities"])` and
  iterate over `valid` instead of `df`
- Cell 18: iterate over `valid` (from cell 16) instead of `df` to avoid
  TypeError on None rows and consistent error-row exclusion

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Greptile feedback on PR #1906 (combined_guardrails_nim.ipynb cell 23).

The batch evaluation loop ran 6 test cases back-to-back with no throttle,
no retry, and no exception handling. Each scenario triggers ~7 sequential
API calls (jailbreak + content safety + topic control + PII input + main
LLM + content safety output + PII output), for ~42 total calls in rapid
succession — reliably exceeding the hosted endpoint's per-minute rate limit.

Add the same three-mechanism robustness pattern used in the single-rail
notebooks:
- Deployment-aware throttle (THROTTLE_S = 0.5 for remote, slightly higher
  than the single-rail 0.3 to account for more calls per scenario)
- Retry-with-exponential-backoff helper (generate_with_retry)
- _Drop429Filter on nemoguardrails.rails.llm.llmrails to suppress verbose
  tracebacks
- Per-scenario try/except so a single failure doesn't crash the batch

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses reviewer feedback on the NIM notebook PR:

- Re-run gliner_pii_detection_nim.ipynb so the previously-empty Smoke
  Test, Running the Rail, Computing Metrics, and Failure Analysis cells
  carry output. Verified the metrics match the Discussion (recall 0.99,
  precision 0.73, the single missed phone-number variant).
- Add an `export NGC_API_KEY="..."` step (plus a note on how it differs
  from NVIDIA_API_KEY) before the `docker run -e NGC_API_KEY` commands in
  content_safety_nim.ipynb and topic_control_nim.ipynb, matching the
  pattern already in gliner_pii_detection_nim.ipynb; switch `docker login`
  to the non-interactive `--password-stdin` form so it uses the same key.
- Fix the content_safety_nim.ipynb smoke-test comment that claimed the
  gaming trash-talk example "should pass through". The over-block on
  Profanity is the intended headline false-positive the surrounding
  narrative and the eval section build on; only the inline comment was
  wrong.
- Remove build_jailbreak_detection_subset.py and jailbreak_detection_subset.csv,
  which are unused by these notebooks and belong with the jailbreak-detection
  notebook PR.

The combined_guardrails_nim.ipynb re-run and scenario fixes are deferred
to a follow-up: a local (non-rate-limited) NIM deployment is needed to
produce a clean batch run and to make each scenario trigger its intended
rail.

Signed-off-by: Sven Chilton <schilton@nvidia.com>
…rst, async

Align the healthcare combined-guardrails example with the rest of the PR and
fix its interactive UX:

- Remove the jailbreak-detection guardrail. This PR's single-subject
  notebooks are content-safety / topic-control / GLiNER-PII (jailbreak is
  split to its own branch), and the local nemoguard-jailbreak-detect NIM does
  not reliably flag prompts. Combined now demonstrates three guardrails —
  Content Safety, Topic Control, and GLiNER PII (input + output).
- Reorder input rails to PII -> content safety -> topic so the dedicated PII
  rail catches identifiers first.
- Four scenarios (safe, content-safety, topic, PII-in-input), each reaching
  its target rail. The PII-in-output scenario is dropped — the input rails
  correctly block every request that would induce PII in a response, so it
  can't be staged cleanly; output PII detection stays configured and is noted
  in the PII-in-input scenario.
- Add the NGC_API_KEY export to the local-deployment instructions.
- Use `await rails.generate_async(...)` and drop nest_asyncio so the notebook
  runs cleanly in Jupyter (nest_asyncio re-entry throws on Python 3.12).

Signed-off-by: Sven Chilton <schilton@nvidia.com>
…syncio)

The single-subject NIM notebooks called sync `rails.generate(...)` under
`nest_asyncio.apply()`, whose event-loop re-entry throws `cannot enter
context` / destroyed-task errors in Jupyter on Python 3.12. Switch the smoke
and evaluation cells to top-level `await rails.generate_async(...)` and remove
nest_asyncio, so they run cleanly in the browser as well as headless. Behavior
is unchanged — eval metrics and Discussions are identical. Covers
gliner_pii_detection, content_safety, and topic_control.

Signed-off-by: Sven Chilton <schilton@nvidia.com>
@svenchilton

Copy link
Copy Markdown
Collaborator Author

@tgasser-nv: My last 3 commits should resolve the 5 points you mentioned in your comment yesterday evening

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants