feat(actions): add RailOutcome rail-result contract#2150
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
1e5839e to
aa8f102
Compare
aa8f102 to
b54604f
Compare
ba19362 to
ef00c0e
Compare
22b20db to
031bfa0
Compare
ef00c0e to
8c1b769
Compare
Greptile SummaryThis PR adds an engine-neutral rail result contract. The main changes are:
|
| Filename | Overview |
|---|---|
| nemoguardrails/actions/rail_outcome.py | Adds the RailOutcome contract, validation, factories, and strict result checking. |
| tests/test_rail_outcome.py | Adds tests for allowed decisions, blocked decisions, transforms, validation errors, and metadata copying. |
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
nemoguardrails/actions/rail_outcome.py:101
**Metadata stays mutable**
This stores the copied metadata as a plain `dict`, so callers can still change an already-created frozen outcome with operations like `outcome.metadata["reason"] = "changed"`, `update()`, or `pop()`. The constructor now protects against later writes to the original input mapping, but the mapping exposed from the outcome itself remains mutable. When an engine or audit path shares the outcome as immutable evidence, a later in-place metadata write can change what other consumers read or log.
Reviews (5): Last reviewed commit: "fix(actions): validate RailOutcome const..." | Re-trigger Greptile
|
@CodeRabbit review |
✅ Action performedReview finished.
|
📝 WalkthroughWalkthroughAdds an immutable, engine-neutral ChangesRail outcome contract
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 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)
nemoguardrails/actions/rail_outcome.py (3)
1-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider declaring
__all__for the public surface.The module exports
RailDecision,RailOutcome,TransformSpec,TransformTarget,require_rail_outcomewithout an explicit__all__. As per coding guidelines, "The public surface is what top-level__all__exports," so an explicit list would make the compatibility-sensitive surface unambiguous for this new module.🤖 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/actions/rail_outcome.py` around lines 1 - 127, Add an explicit top-level __all__ in rail_outcome.py listing the intended public symbols: RailDecision, TransformTarget, TransformSpec, RailOutcome, and require_rail_outcome. Keep internal implementation details excluded and preserve the existing definitions and behavior.Source: Coding guidelines
82-84: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winNo protection against duplicate transform targets.
__post_init__only checks non-empty-iff-TRANSFORM; it doesn't rejecttransformswith repeatedTransformTargetentries.transform_text(line 97) builds a dict via comprehension, so a duplicate target silently overwrites the earlier rewrite rather than surfacing an error.🛡️ Add uniqueness check
def __post_init__(self) -> None: if bool(self.transforms) != (self.decision is RailDecision.TRANSFORM): raise ValueError("transforms must be non-empty if and only if decision is TRANSFORM") + targets = [spec.target for spec in self.transforms] + if len(targets) != len(set(targets)): + raise ValueError("transforms must not repeat the same TransformTarget")Also applies to: 96-97
🤖 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/actions/rail_outcome.py` around lines 82 - 84, Update RailOutcome.__post_init__ to validate that transforms contains no duplicate TransformTarget entries, while preserving the existing TRANSFORM/non-empty consistency check. Reject repeated targets with a ValueError before transform_text’s dictionary construction can silently overwrite earlier rewrites.
68-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
metadatadict is mutable despite "immutable" contract.The dataclass is
frozen=True/slots=True, butmetadatais a plaindict. Callers can still dooutcome.metadata["k"] = "v"after construction, silently breaking the immutability guarantee this module (andtest_outcome_is_immutable) advertises.🔒 Use a read-only mapping
+from types import MappingProxyType ... - metadata: dict[str, Any] = field(default_factory=dict) + metadata: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({})) ... - return cls(decision=RailDecision.ALLOW, reason=reason, metadata=dict(metadata)) + return cls(decision=RailDecision.ALLOW, reason=reason, metadata=MappingProxyType(dict(metadata)))(apply similarly to
blockandtransform)Also applies to: 99-120
🤖 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/actions/rail_outcome.py` around lines 68 - 80, Update RailOutcome and the related block/transform outcome dataclasses to store metadata as a read-only mapping rather than a mutable dict, while preserving the existing default-empty behavior and metadata access API. Ensure metadata is defensively copied or wrapped during construction so callers cannot mutate internal state after creation, including through the default factory.
🤖 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 `@nemoguardrails/actions/rail_outcome.py`:
- Around line 29-31: Rewrite the TRANSFORM section of the module docstring in
rail_outcome.py to clearly state that transform outcomes are non-streaming,
while streaming output bypasses them as non-blocking and does not apply
rewrites. Preserve the documented behavior and scope; only correct the grammar
and clarity.
---
Nitpick comments:
In `@nemoguardrails/actions/rail_outcome.py`:
- Around line 1-127: Add an explicit top-level __all__ in rail_outcome.py
listing the intended public symbols: RailDecision, TransformTarget,
TransformSpec, RailOutcome, and require_rail_outcome. Keep internal
implementation details excluded and preserve the existing definitions and
behavior.
- Around line 82-84: Update RailOutcome.__post_init__ to validate that
transforms contains no duplicate TransformTarget entries, while preserving the
existing TRANSFORM/non-empty consistency check. Reject repeated targets with a
ValueError before transform_text’s dictionary construction can silently
overwrite earlier rewrites.
- Around line 68-80: Update RailOutcome and the related block/transform outcome
dataclasses to store metadata as a read-only mapping rather than a mutable dict,
while preserving the existing default-empty behavior and metadata access API.
Ensure metadata is defensively copied or wrapped during construction so callers
cannot mutate internal state after creation, including through the default
factory.
🪄 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: 81cdf5e5-d181-4041-9062-1123119e55cf
📒 Files selected for processing (2)
nemoguardrails/actions/rail_outcome.pytests/test_rail_outcome.py
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
5e1c9d9 to
ae7bc3f
Compare
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
55a02d7 to
62d119d
Compare
tgasser-nv
left a comment
There was a problem hiding this comment.
Looks good! I had some pending feedback on replacing **metadata kwargs with a dict and the setitem() breaking deepcopy but these are both address now.
Description
Introduce the engine-neutral
RailOutcomecontract for rail decisions: allow,block, and transform, with neutral reasons, metadata, and transform targets.
The strict validator rejects legacy action return values. This is the contract
used by the migration in #2151
AI Assistance
Checklist
Summary by CodeRabbit
New Features
Tests