feat(iorails): Support blocking rails - #2264
Conversation
126f981 to
e31419e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
e31419e to
b560e84
Compare
…use a pooled HTTP client for API actions
b560e84 to
630c1d3
Compare
Greptile SummaryThis PR expands manifest-driven IORails support to blocking input and output rails while adding request-time context bindings, synchronous-action handling, and compile-time configuration validation.
|
| Filename | Overview |
|---|---|
| nemoguardrails/guardrails/compiled_rail.py | Adds per-request context resolution, synchronous-action support, surface gating, and compile-time model and dependency validation. |
| nemoguardrails/guardrails/iorails.py | Replaces the fixed surface allowlist with manifest-driven trial compilation using configuration-aware dependencies. |
| nemoguardrails/library/jailbreak_detection/rail.py | Aligns declared jailbreak dependencies with the supported local model path by removing unused scikit-learn and retaining torch and transformers. |
| nemoguardrails/guardrails/guardrails_types.py | Expands the allowlist of evidence fields exposed for blocked rail outcomes. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Guardrails configuration] --> B[IORails compatibility gate]
B --> C{Surface is servable?}
C -->|No| D[Fall back to LLMRails or raise when required]
C -->|Yes| E[Compile manifest rail]
E --> F[Validate bindings, models, and dependencies]
F --> G[Resolve request context]
G --> H[Invoke sync or async action]
H --> I[Allow or block outcome]
Reviews (8): Last reviewed commit: "Remove unused scikit-learn dependency" | Re-trigger Greptile
📝 WalkthroughWalkthroughChangesThe change adds request-time context bindings for compiled rails, validates model and optional dependencies during compilation, expands IORails surface support, and adds cross-engine tests for local, model-backed, and vendor rails. Rail compilation and cross-engine parity
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Request
participant CompiledRail
participant Action
Request->>CompiledRail: Provide user_message or bot_message
CompiledRail->>Action: Inject request-time bound parameters
Action-->>CompiledRail: Return RailOutcome
CompiledRail-->>Request: Return rail result
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
tests/guardrails/test_cross_engine_vendor_rails.py (1)
136-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
envdefaults toNonebut every reader calls.items()on it.Lines 505, 531, and 556 iterate
rail.env.items()without a guard. Every current entry inVENDOR_RAILSpassesenv, so the tests pass today. A new entry that omitsenvfails withAttributeErrorinstead of a useful message.Default both fields to an empty dict. That also removes the two
type: ignoresuppressions.♻️ Proposed change
- rails_config: dict = None # type: ignore[assignment] - env: dict = None # type: ignore[assignment] + rails_config: dict = field(default_factory=dict) + env: dict = field(default_factory=dict)Add
fieldto the existing import:-from dataclasses import dataclass +from dataclasses import dataclass, field🤖 Prompt for 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. In `@tests/guardrails/test_cross_engine_vendor_rails.py` around lines 136 - 139, Update the rail configuration defaults for rails_config and env to empty dictionaries instead of None, using dataclass field defaults as needed to avoid shared mutable state. Remove the corresponding type: ignore suppressions, while preserving the existing .items() readers in the rail validation logic.tests/guardrails/test_cross_engine_model_rails.py (1)
139-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe shared prompt template hides whether the checked text reaches the rail.
Line 139 uses
{{ user_input }}for all four tasks, includingllama_guard_check_outputandself_check_output. Those tasks check the bot response. Jinja renders an undefined variable as an empty string, so the prompt still forms and the canned completion decides the verdict on both engines.The result is that an engine which failed to pass the bot response would still pass this test. The vendor file guards the same risk with
test_both_engines_send_the_vendor_the_same_request. Use a direction-appropriate template so the model input carries the text under check.♻️ Proposed change
- prompt: dict = {"task": rail.prompt_task, "content": "Check the input: {{ user_input }}\nAnswer [yes/no]:"} + checked_text = "{{ bot_response }}" if rail.direction == "output" else "{{ user_input }}" + prompt: dict = {"task": rail.prompt_task, "content": f"Check: {checked_text}\nAnswer [yes/no]:"}🤖 Prompt for 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. In `@tests/guardrails/test_cross_engine_model_rails.py` around lines 139 - 147, Update the shared prompt construction around rail.prompt_task so input rails use user_input while output rails (llama_guard_check_output and self_check_output) use the bot response variable expected by those tasks. Ensure the rendered prompt always contains the text under check, preserving the existing prompt structure and per-rail configuration.nemoguardrails/guardrails/compiled_rail.py (1)
243-252: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftDispatch synchronous actions off the event loop
validate_guardrails_ai_inputandvalidate_guardrails_ai_outputcallGuard.validatesynchronously. A blocking validator can stall every request on the event loop. Run synchronous actions withasyncio.to_thread, then await any awaitable result they return. Add tests for event-loop responsiveness and context propagation.🤖 Prompt for 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. In `@nemoguardrails/guardrails/compiled_rail.py` around lines 243 - 252, Update _invoke to execute synchronous actions through asyncio.to_thread, while continuing to await results from asynchronous actions. Preserve the existing _call_kwargs arguments and support actions that return awaitables after thread execution. Add coverage for event-loop responsiveness during a blocking validator and propagation of the relevant context into the worker thread.
🤖 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 `@tests/guardrails/test_guardrails.py`:
- Around line 421-425: Update the skipif condition for “gcpnlp moderation
detailed” to catch ModuleNotFoundError from find_spec("google.cloud.language")
when the parent package is missing, treating that exception as an unavailable
dependency while preserving the existing skip reason; use uv run --locked for
verification.
---
Nitpick comments:
In `@nemoguardrails/guardrails/compiled_rail.py`:
- Around line 243-252: Update _invoke to execute synchronous actions through
asyncio.to_thread, while continuing to await results from asynchronous actions.
Preserve the existing _call_kwargs arguments and support actions that return
awaitables after thread execution. Add coverage for event-loop responsiveness
during a blocking validator and propagation of the relevant context into the
worker thread.
In `@tests/guardrails/test_cross_engine_model_rails.py`:
- Around line 139-147: Update the shared prompt construction around
rail.prompt_task so input rails use user_input while output rails
(llama_guard_check_output and self_check_output) use the bot response variable
expected by those tasks. Ensure the rendered prompt always contains the text
under check, preserving the existing prompt structure and per-rail
configuration.
In `@tests/guardrails/test_cross_engine_vendor_rails.py`:
- Around line 136-139: Update the rail configuration defaults for rails_config
and env to empty dictionaries instead of None, using dataclass field defaults as
needed to avoid shared mutable state. Remove the corresponding type: ignore
suppressions, while preserving the existing .items() readers in the rail
validation logic.
🪄 Autofix
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: ca72b73a-4004-4a4d-8723-56482c60227a
📒 Files selected for processing (9)
nemoguardrails/guardrails/compiled_rail.pynemoguardrails/guardrails/guardrails_types.pynemoguardrails/guardrails/iorails.pytests/guardrails/test_compiled_rail.pytests/guardrails/test_cross_engine_local_rails.pytests/guardrails/test_cross_engine_model_rails.pytests/guardrails/test_cross_engine_vendor_rails.pytests/guardrails/test_guardrails.pytests/guardrails/test_guardrails_types.py
…ed with current manifest ''
Description
This PR adds support for rails that can block input (prompt) or output (response) using the new Rail Manifest system along with the CompiledRail approach. This is part of a stack as shown below, with future PRs to come:
PR 1 #2241
PR 2 #2246
PR 3a #2253
PR 3b #2261 . Builds on the #2253 and migrates from RailAction subclasses to CompiledRail implementations for all currently-supported actions.
PR 4 #2264 enable the 49 block-only input/output surfaces via catalog-derived gating
PR 4.5 Use
RailOutcomeinstead ofRailResultPR 5 transform surfaces (18): RailResult.transforms, rewrite threading, MODIFIED status
PR 6 model_caches response-cache parity with LLMRails
Related Issue(s)
Verification
Pre-commit
Unit-test
Integration test with Chat
AI Assistance
Checklist
Summary by CodeRabbit
New Features
Bug Fixes