feat(examples): introduce NIM-based example notebooks, retire superseded ones (NGUARD-724) - #1906
Conversation
📝 WalkthroughWalkthroughThis 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. ChangesNeMo Guardrails NIM Example Notebooks and Evaluation Datasets
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (4)
examples/notebooks/data/content_safety_subset.csvis excluded by!**/*.csvexamples/notebooks/data/jailbreak_detection_subset.csvis excluded by!**/*.csvexamples/notebooks/data/pii_detection_subset.csvis excluded by!**/*.csvexamples/notebooks/data/topic_control_subset.csvis excluded by!**/*.csv
📒 Files selected for processing (10)
examples/notebooks/combined_guardrails_nim.ipynbexamples/notebooks/content_safety_nim.ipynbexamples/notebooks/content_safety_tutorial.ipynbexamples/notebooks/data/build_content_safety_subset.pyexamples/notebooks/data/build_jailbreak_detection_subset.pyexamples/notebooks/data/build_pii_detection_subset.pyexamples/notebooks/data/build_topic_control_subset.pyexamples/notebooks/gliner_pii_detection.ipynbexamples/notebooks/gliner_pii_detection_nim.ipynbexamples/notebooks/topic_control_nim.ipynb
💤 Files with no reviewable changes (1)
- examples/notebooks/gliner_pii_detection.ipynb
Greptile SummaryThis PR introduces three NIM-based example notebooks (
|
| 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
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
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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>
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>
c26eccd to
f903854
Compare
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>
9c98872 to
8187c71
Compare
… 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>
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>
tgasser-nv
left a comment
There was a problem hiding this comment.
Looks good! Just a few items to clean up before merging:
- The
gliner_pii_detection_nim.ipynbnotebook 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? - The content_safety_nim.ipynb, topic_control_nim.ipynb, and combined_guardrails_nim.ipynb all
docker runinstructions with-e NGC_API_KEY, but noexport NGC_API_KEY="..."in a previous instruction. Thegliner_pii_detection_nim.ipynbnotebook does include this at the top of the "Local Deployment" section. - In
content_safety_nim.ipynbUnder 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. - Could you run all cells in
combined_guardrails_nim.ipynb? Only the first two were run as-is. - Are the jailbreak-related scripts used in this PR? Recommend removing the
build_jailbreak_detection_subset.pyandjailbreak_detection_subset.csvfiles and adding them in the jailbreak-specific PR.
…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>
|
@tgasser-nv: My last 3 commits should resolve the 5 points you mentioned in your comment yesterday evening |
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
entitiesfocus list, email-local substring double-detection, over-broadvehicle_identifier/street_address/phone_numberspans).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):
Shared eval-loop infrastructure across all three notebooks:
Deferred to follow-on commits:
@tgasser-nv, @Pouyanpi, and/or @miyoungc, please review.
Related Issue(s)
Checklist
Summary by CodeRabbit