Skip to content

fix(streaming): pass user content to output rails#2081

Open
Pouyanpi wants to merge 1 commit into
developfrom
fix/streaming-output-rails-user-content
Open

fix(streaming): pass user content to output rails#2081
Pouyanpi wants to merge 1 commit into
developfrom
fix/streaming-output-rails-user-content

Conversation

@Pouyanpi

@Pouyanpi Pouyanpi commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Description

pass user content to output rails, fixes a bug that discovered in #1978 , addresses #1978 (comment).

Root cause

streaming output rails received the wrong shape for context["user_message"]. The
nested helper _get_latest_user_message() returned the whole message dict, so for
stream_async(messages=[...]) calls the output-rail context (and the $user_message
action param) got {"role": "user", "content": "hello"} instead of "hello". That
malformed value reaches runtime execution: both content safety and self check output
template user_message into their provider prompts, and custom actions reading
context["user_message"] get a dict. Rails whose decision depends only on bot_message
still behaved correctly, which is why it stayed latent.

Fix: return message["content"] (or "" when there is no user message).

scope: streaming output rails with messages=[...] only. stream_async(prompt="...")
short-circuits to the string and was unaffected; non-streaming rails resolve
$user_message via the Colang runtime, a different path.

Related Issue(s)

  • Fixes #<issue_number>
  • Issue assignee: @Pouyanpi

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: Claude Code).

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed streaming output rails so the latest user message is passed as plain text, improving compatibility with downstream context handling.
    • Ensured output rails can access the incoming user message during streaming from an external token generator.
  • Tests

    • Added coverage for streaming output rails to confirm token output remains unchanged and the user message is available as expected.

@github-actions github-actions Bot added status: needs triage New issues that have not yet been reviewed or categorized. size: S labels Jun 26, 2026
@Pouyanpi Pouyanpi self-assigned this Jun 26, 2026
@Pouyanpi Pouyanpi added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Jun 26, 2026
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug in streaming output rails where _get_latest_user_message returned the full {"role": "user", "content": "..."} dict instead of the plain string content, causing downstream consumers (content-safety templates, self check output, and custom actions reading context["user_message"]) to receive a malformed dict.

  • _get_latest_user_message now returns message.get("content", "") and has its return type updated from dict to str, matching both the fix and the empty-string fallback when no user message exists.
  • Two new pytest.mark.asyncio tests cover the corrected behavior: one asserts the content string propagates into action context, and one asserts the empty-string fallback when the message list contains no user-role entry.

Confidence Score: 5/5

Safe to merge — the change is a narrow, targeted fix with no side effects on existing call sites.

The fix is minimal (4 lines changed in one helper) and both call sites already used the return value as a string. The old code returned a dict where a string was expected; the new code returns the plain content string, which is exactly what templates and action params need. New tests cover both the happy path and the no-user-message fallback.

No files require special attention.

Important Files Changed

Filename Overview
nemoguardrails/rails/llm/llmrails.py Fixed _get_latest_user_message to return message.get("content", "") (str) instead of the full message dict; both callers (_prepare_context_for_parallel_rails and _prepare_params) now receive the correct string value for user_message.
tests/test_streaming_output_rails.py Two new async tests added: one verifies the user message string is propagated correctly to output rail context, and one verifies the empty-string fallback when no user message is present in the messages list.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["stream_async(messages=[...])"] --> B["_prepare_context_for_parallel_rails\n/ _prepare_params"]
    B --> C["_get_latest_user_message(messages)"]
    C --> D{User message\nfound?}
    D -- "Yes (before fix)" --> E["return message dict\n{role, content}"]
    D -- "Yes (after fix)" --> F["return message.get('content', '')"]
    D -- "No" --> G["return ''"]
    E --> H["context['user_message'] = dict ❌\nTemplates / actions receive wrong type"]
    F --> I["context['user_message'] = str ✅\nTemplates / actions receive correct string"]
    G --> I
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["stream_async(messages=[...])"] --> B["_prepare_context_for_parallel_rails\n/ _prepare_params"]
    B --> C["_get_latest_user_message(messages)"]
    C --> D{User message\nfound?}
    D -- "Yes (before fix)" --> E["return message dict\n{role, content}"]
    D -- "Yes (after fix)" --> F["return message.get('content', '')"]
    D -- "No" --> G["return ''"]
    E --> H["context['user_message'] = dict ❌\nTemplates / actions receive wrong type"]
    F --> I["context['user_message'] = str ✅\nTemplates / actions receive correct string"]
    G --> I
Loading

Reviews (2): Last reviewed commit: "fix(streaming): pass user content to out..." | Re-trigger Greptile

Comment thread nemoguardrails/rails/llm/llmrails.py Outdated
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The helper used by output-rails streaming now returns the latest user message content string, or an empty string when unavailable. A new async streaming test verifies context.user_message and token output for an external generator.

Changes

Streaming user content propagation

Layer / File(s) Summary
User message content in streaming
nemoguardrails/rails/llm/llmrails.py, tests/test_streaming_output_rails.py
_get_latest_user_message now returns user content as a string or "", and a new streaming test checks that context.user_message contains "Hello" while streamed tokens remain unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: passing user content to streaming output rails.
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 Minor bug fix: one helper return-shape change plus a focused regression test; PR description notes added tests and verification beyond CI.
✨ 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 fix/streaming-output-rails-user-content

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

@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.

🧹 Nitpick comments (1)
nemoguardrails/rails/llm/llmrails.py (1)

1814-1820: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Helper now correctly returns the user message content string (or ""), matching the downstream string contract for context["user_message"] and $user_message resolution.

Optional: the -> Any annotation is now imprecise since this always returns a string. Tightening it to -> str better documents the contract for callers like _prepare_params/_prepare_context_for_parallel_rails.

🤖 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/rails/llm/llmrails.py` around lines 1814 - 1820, The helper
that extracts the latest user message currently has an overly broad return
annotation, even though it always returns a string or an empty string. Update
the return type on the user-message helper in LLMRails to str so it matches the
actual contract used by _prepare_params and _prepare_context_for_parallel_rails,
while keeping the current string-return behavior unchanged.
🤖 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 `@nemoguardrails/rails/llm/llmrails.py`:
- Around line 1814-1820: The helper that extracts the latest user message
currently has an overly broad return annotation, even though it always returns a
string or an empty string. Update the return type on the user-message helper in
LLMRails to str so it matches the actual contract used by _prepare_params and
_prepare_context_for_parallel_rails, while keeping the current string-return
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: eeafdb04-e4bc-42da-ac54-56d58a23bff2

📥 Commits

Reviewing files that changed from the base of the PR and between 15f87ee and 69745f6.

📒 Files selected for processing (2)
  • nemoguardrails/rails/llm/llmrails.py
  • tests/test_streaming_output_rails.py

@Pouyanpi Pouyanpi force-pushed the fix/streaming-output-rails-user-content branch from 69745f6 to 81ef48f Compare June 26, 2026 09:56
@github-actions github-actions Bot added size: M and removed size: S labels Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: M status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant