fix: prevent stale action params in streaming output rails across chunks#2006
fix: prevent stale action params in streaming output rails across chunks#2006nac7 wants to merge 3 commits into
Conversation
get_action_details_from_flow_id() returns the live dict from the parsed
flow config. _prepare_params() was mutating it in place when substituting
$bot_message/$user_message placeholders, so every chunk after the first
received the first chunk's already-substituted value instead of the
current chunk. Shallow-copy action_params at the start of _prepare_params
so the original flow config dict is never modified.
Also removes the mutable default argument `action_params: Dict = {}`.
Closes NVIDIA-NeMo#1935
📝 WalkthroughWalkthroughThis PR fixes a parameter mutation bug in streaming output rails where placeholder-substituted values ( ChangesStreaming output rails parameter mutation fix
🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_streaming_output_rails.py (1)
623-625: 💤 Low valueOptional: Add parentheses for operator precedence clarity.
The assertion logic is correct, but the mixed
and/orprecedence is flagged by static analysis. Adding explicit parentheses improves readability without changing behavior.♻️ Proposed refactor for clarity
- assert isinstance(received, dict) and received.get("content") == "hello there" or received == "hello there", ( + assert (isinstance(received, dict) and received.get("content") == "hello there") or received == "hello there", ( f"$user_message was not substituted correctly: {received!r}" )🤖 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/test_streaming_output_rails.py` around lines 623 - 625, The assertion mixes and/or which can be ambiguous to linters; wrap the boolean subexpressions in parentheses to make operator precedence explicit—specifically modify the assert that checks the variable received (the expression beginning with assert isinstance(received, dict)...) so the dict-check and content-check are grouped together and the alternative equality check (received == "hello there") is grouped, preserving the same logic but clarifying precedence for static analysis.Source: Linters/SAST tools
🤖 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.
Nitpick comments:
In `@tests/test_streaming_output_rails.py`:
- Around line 623-625: The assertion mixes and/or which can be ambiguous to
linters; wrap the boolean subexpressions in parentheses to make operator
precedence explicit—specifically modify the assert that checks the variable
received (the expression beginning with assert isinstance(received, dict)...) so
the dict-check and content-check are grouped together and the alternative
equality check (received == "hello there") is grouped, preserving the same logic
but clarifying precedence for static analysis.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 7beaebd7-29c6-4191-a29e-548ba09d4572
📒 Files selected for processing (2)
nemoguardrails/rails/llm/llmrails.pytests/test_streaming_output_rails.py
Greptile SummaryThis PR fixes a stale-parameter bug in streaming output rails where
|
| Filename | Overview |
|---|---|
| nemoguardrails/rails/llm/llmrails.py | Fixes mutable default argument and in-place mutation of the live flow-config dict in _prepare_params; shallow copy is sufficient since all substituted values are immutable strings. |
| tests/test_streaming_output_rails.py | Adds three targeted regression tests covering stale-params across chunks, flow-config immutability, and $user_message substitution; all include return_exceptions=True on the teardown gather. |
| .github/workflows/_test.yml | Downgrades codecov/codecov-action from v5 to v4 with no explanation in the PR description — unrelated to the bug fix and may be accidental. |
Sequence Diagram
sequenceDiagram
participant SH as StreamingHandler
participant SA as stream_async
participant PP as _prepare_params
participant FC as flow config dict<br/>(action_params)
participant AC as registered action
loop each chunk batch
SH->>SA: chunk_batch ("alpha ", "beta", …)
SA->>SA: "get_action_details_from_flow_id()<br/>returns live FC reference"
SA->>PP: "flow_id, action_name,<br/>bot_response_chunk,<br/>action_params (FC ref)"
PP->>PP: "action_params = dict(action_params or {})<br/>← shallow copy (NEW)"
PP->>PP: "substitute $bot_message → chunk text<br/>on the COPY, not FC"
note over FC: FC["chunk"] stays "$bot_message" ✓
PP-->>SA: resolved params dict
SA->>AC: "call action(chunk="alpha ")<br/>call action(chunk="beta")"
end
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
.github/workflows/_test.yml:107
**Unexplained downgrade of `codecov-action`**
This PR downgrades `codecov/codecov-action` from `v5` to `v4`, but the change isn't mentioned anywhere in the PR description or commit message. This is unrelated to the stale-action-params fix. If this was intentional (e.g., to work around a v5 regression), a brief comment explaining why would help reviewers — and if it's accidental, it should be reverted to avoid silently rolling back any v5 improvements.
Reviews (2): Last reviewed commit: "fix: address review comments and resolve..." | Re-trigger Greptile
- Add return_exceptions=True to asyncio.gather calls in three tests to prevent stray CancelledError from pytest-asyncio teardown tasks from surfacing as unexpected test failures - Add explicit parentheses to the isinstance/or assertion to make operator precedence unambiguous - Downgrade codecov-action from v5 to v4 to fix GPG key import failure on Python 3.11 CI runs
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Hi @Pouyanpi , if you have some time, could you please help with this PR review? Thanks! |
Summary
Fixes #1935.
When streaming output rails invoke an action with explicit colang params (e.g.
execute capture_output(text=$bot_message)), the$bot_messageplaceholder is resolved inside_prepare_params(). The bug:get_action_details_from_flow_id()returns the live dict from the parsed flow config — not a copy — so_prepare_params()was mutating it in place. After the first chunk,action_params["text"]permanently stored the first chunk's text instead of the"$bot_message"sentinel. Every subsequent chunk received the stale first-chunk value.Root causes fixed:
_prepare_params()mutatedaction_paramsin place (the live flow-config dict)action_params: Dict[str, Any] = {}(secondary issue)Fix: shallow-copy
action_paramsat the start of_prepare_params()before any substitution, so the original parsed-flow dict is never touched.Affected rails (any that pass
$bot_messageor$user_messageas explicit action params):prompt_security,privateai,gliner,policyai,trend_micro,activefence,ai_defense,regex.Changes
nemoguardrails/rails/llm/llmrails.py: addaction_params = dict(action_params or {})shallow copy + fix mutable default argtests/test_streaming_output_rails.py: 3 new regression teststest_streaming_action_params_not_stale_across_chunks— verifies each chunk batch gets the correct substituted value (not stale)test_streaming_action_params_original_flow_config_not_mutated— verifies the parsed flow config dict is not mutated after streamingtest_streaming_user_message_param_substituted— covers the$user_messagesubstitution branchTest plan
tests/test_streaming_output_rails.pypassruff checkpasses on both changed filesSummary by CodeRabbit
Bug Fixes
Tests