Skip to content

Commit 031bfa0

Browse files
committed
fix(streaming): fail closed when rail actions fail
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
1 parent 5331054 commit 031bfa0

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

nemoguardrails/rails/llm/llmrails.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2015,9 +2015,27 @@ def _prepare_params(
20152015
action_params=action_params,
20162016
)
20172017

2018-
result = await self.runtime.action_dispatcher.execute_action(action_name, params)
2018+
try:
2019+
result, status = await self.runtime.action_dispatcher.execute_action(action_name, params)
2020+
except Exception:
2021+
log.exception("Action %s failed during sequential streaming", action_name)
2022+
result, status = None, "failed"
20192023
self._explain_info = self._ensure_explain_info()
20202024

2025+
if status != "success":
2026+
error_message = f"Action {action_name} failed with status: {status}"
2027+
log.error(error_message)
2028+
error_data = {
2029+
"error": {
2030+
"message": f"Internal error in {flow_id} rail: {error_message}",
2031+
"type": "internal_error",
2032+
"param": flow_id,
2033+
"code": "rail_execution_failure",
2034+
}
2035+
}
2036+
yield json.dumps(error_data)
2037+
return
2038+
20212039
action_func = self.runtime.action_dispatcher.get_action(action_name)
20222040

20232041
# Use the mapping to decide if the result indicates blocked content.

tests/test_streaming_internal_errors.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from nemoguardrails import RailsConfig
2525
from nemoguardrails.actions import action
26+
from nemoguardrails.exceptions import LLMCallException
2627
from nemoguardrails.imports import check_optional_dependency
2728
from tests.utils import TestChat
2829

@@ -109,6 +110,67 @@ def failing_rail_action(**params):
109110
assert error["error"]["param"] == "failing safety check"
110111

111112

113+
@pytest.mark.asyncio
114+
@pytest.mark.parametrize(
115+
"action_error",
116+
[
117+
pytest.param(RuntimeError("Action execution failed"), id="runtime-error"),
118+
pytest.param(LLMCallException(RuntimeError("LLM call failed")), id="llm-call-error"),
119+
],
120+
)
121+
async def test_sequential_streaming_action_execution_failure(action_error: Exception):
122+
"""Test fail-closed behavior in sequential streaming when a rail action fails."""
123+
124+
@action(is_system_action=True)
125+
def failing_rail_action(**params):
126+
raise action_error
127+
128+
config = RailsConfig.from_content(
129+
config={
130+
"models": [{"type": "main", "engine": "openai", "model": "gpt-3.5-turbo"}],
131+
"rails": {
132+
"output": {
133+
"parallel": False,
134+
"flows": ["failing safety check"],
135+
"streaming": {
136+
"enabled": True,
137+
"chunk_size": 4,
138+
},
139+
}
140+
},
141+
},
142+
colang_content="""
143+
define user express greeting
144+
"hi"
145+
define flow
146+
user express greeting
147+
bot express greeting
148+
149+
define subflow failing safety check
150+
execute failing_rail_action
151+
""",
152+
)
153+
154+
llm_completions = [
155+
'bot express greeting\n "Hello there! How can I help you?"',
156+
]
157+
158+
chat = TestChat(config, llm_completions=llm_completions, streaming=True)
159+
chat.app.register_action(failing_rail_action)
160+
161+
chunks = await collect_streaming_chunks(chat.app.stream_async(messages=[{"role": "user", "content": "Hi!"}]))
162+
163+
internal_error_chunks = find_internal_error_chunks(chunks)
164+
assert len(internal_error_chunks) == 1
165+
error = internal_error_chunks[0]["error"]
166+
assert error == {
167+
"message": "Internal error in failing safety check rail: Action failing_rail_action failed with status: failed",
168+
"type": "internal_error",
169+
"param": "failing safety check",
170+
"code": "rail_execution_failure",
171+
}
172+
173+
112174
@pytest.mark.asyncio
113175
async def test_streaming_internal_error_format():
114176
"""Test that streaming internal errors have the correct format."""

0 commit comments

Comments
 (0)