Skip to content

Commit 0139dc4

Browse files
committed
test(self-check): add openai config smoke coverage
1 parent 2895b99 commit 0139dc4

1 file changed

Lines changed: 361 additions & 0 deletions

File tree

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import os
17+
from unittest.mock import patch
18+
19+
import pytest
20+
21+
from nemoguardrails import LLMRails, RailsConfig
22+
from nemoguardrails.imports import check_optional_dependency
23+
from nemoguardrails.testing.fake_model import FakeLLMModel
24+
25+
COLANG = """
26+
define user express greeting
27+
"hello"
28+
"hi"
29+
30+
define bot express greeting
31+
"Hey!"
32+
33+
define flow greeting
34+
user express greeting
35+
bot express greeting
36+
"""
37+
38+
OPENAI_MODEL = """
39+
models:
40+
- type: main
41+
engine: openai
42+
model: gpt-4o-mini
43+
"""
44+
45+
LIVE_TEST_MODE = os.environ.get("LIVE_TEST_MODE") or os.environ.get("TEST_LIVE_MODE") or os.environ.get("LIVE_TEST")
46+
47+
48+
def _response_content(response):
49+
return response["content"] if isinstance(response, dict) else response.response[0]["content"]
50+
51+
52+
@patch("nemoguardrails.rails.llm.llmrails.init_llm_model")
53+
def test_llmrails_openai_default_self_check_uses_main_model(mock_init_llm_model):
54+
main_llm = FakeLLMModel(responses=["Yes"])
55+
mock_init_llm_model.return_value = main_llm
56+
config = RailsConfig.from_content(
57+
colang_content=COLANG,
58+
yaml_content=OPENAI_MODEL
59+
+ """
60+
rails:
61+
input:
62+
flows:
63+
- self check input
64+
prompts:
65+
- task: self_check_input
66+
content: |
67+
User message: "{{ user_input }}"
68+
Answer No.
69+
enable_rails_exceptions: True
70+
""",
71+
)
72+
73+
rails = LLMRails(config, verbose=False)
74+
response = rails.generate(messages=[{"role": "user", "content": "hello"}])
75+
76+
assert response["role"] == "exception"
77+
assert response["content"]["type"] == "InputRailException"
78+
assert main_llm.inference_count == 1
79+
assert mock_init_llm_model.call_args.kwargs["provider_name"] == "openai"
80+
81+
82+
@patch("nemoguardrails.rails.llm.llmrails.init_llm_model")
83+
def test_llmrails_openai_custom_self_check_models_route_by_task(mock_init_llm_model):
84+
main_llm = FakeLLMModel(responses=[" ask question", " Here is the answer."])
85+
check_harmful_llm = FakeLLMModel(responses=["No"])
86+
check_data_leakage_llm = FakeLLMModel(responses=["No"])
87+
mock_init_llm_model.side_effect = [main_llm, check_harmful_llm, check_data_leakage_llm]
88+
config = RailsConfig.from_content(
89+
colang_content="""
90+
define user ask question
91+
"tell me something"
92+
93+
define flow
94+
user ask question
95+
bot respond
96+
""",
97+
yaml_content="""
98+
models:
99+
- type: main
100+
engine: openai
101+
model: gpt-4o-mini
102+
- type: check_harmful
103+
engine: openai
104+
model: gpt-4o-mini
105+
- type: check_data_leakage
106+
engine: openai
107+
model: gpt-4o-mini
108+
rails:
109+
input:
110+
flows:
111+
- self check input $task=check_harmful
112+
output:
113+
flows:
114+
- self check output $task=check_data_leakage
115+
prompts:
116+
- task: check_harmful
117+
content: |
118+
User message: "{{ user_input }}"
119+
Answer No.
120+
- task: check_data_leakage
121+
content: |
122+
Bot response: "{{ bot_response }}"
123+
Answer No.
124+
enable_rails_exceptions: True
125+
""",
126+
)
127+
128+
rails = LLMRails(config, verbose=False)
129+
response = rails.generate(messages=[{"role": "user", "content": "tell me something"}])
130+
131+
assert _response_content(response) == "Here is the answer."
132+
assert check_harmful_llm.inference_count == 1
133+
assert check_data_leakage_llm.inference_count == 1
134+
assert [call.kwargs["provider_name"] for call in mock_init_llm_model.call_args_list] == [
135+
"openai",
136+
"openai",
137+
"openai",
138+
]
139+
140+
141+
@patch("nemoguardrails.rails.llm.llmrails.init_llm_model")
142+
def test_llmrails_openai_custom_task_falls_back_to_default_self_check_model(mock_init_llm_model):
143+
main_llm = FakeLLMModel(responses=[])
144+
default_self_check_llm = FakeLLMModel(responses=["Yes"])
145+
mock_init_llm_model.side_effect = [main_llm, default_self_check_llm]
146+
config = RailsConfig.from_content(
147+
colang_content=COLANG,
148+
yaml_content="""
149+
models:
150+
- type: main
151+
engine: openai
152+
model: gpt-4o-mini
153+
- type: self_check_input
154+
engine: openai
155+
model: gpt-4o-mini
156+
rails:
157+
input:
158+
flows:
159+
- self check input $task=check_harmful
160+
prompts:
161+
- task: check_harmful
162+
content: |
163+
User message: "{{ user_input }}"
164+
Answer No.
165+
enable_rails_exceptions: True
166+
""",
167+
)
168+
169+
rails = LLMRails(config, verbose=False)
170+
response = rails.generate(messages=[{"role": "user", "content": "hello"}])
171+
172+
assert response["role"] == "exception"
173+
assert response["content"]["type"] == "InputRailException"
174+
assert main_llm.inference_count == 0
175+
assert default_self_check_llm.inference_count == 1
176+
177+
178+
@patch("nemoguardrails.rails.llm.llmrails.init_llm_model")
179+
def test_llmrails_openai_parallel_input_model_routes_by_task(mock_init_llm_model):
180+
main_llm = FakeLLMModel(responses=[" express greeting", ' "Hey!"'])
181+
check_harmful_llm = FakeLLMModel(responses=["No"])
182+
default_input_llm = FakeLLMModel(responses=["Yes"])
183+
mock_init_llm_model.side_effect = [main_llm, check_harmful_llm, default_input_llm]
184+
config = RailsConfig.from_content(
185+
colang_content=COLANG,
186+
yaml_content="""
187+
models:
188+
- type: main
189+
engine: openai
190+
model: gpt-4o-mini
191+
- type: check_harmful
192+
engine: openai
193+
model: gpt-4o-mini
194+
- type: self_check_input
195+
engine: openai
196+
model: gpt-4o-mini
197+
rails:
198+
input:
199+
parallel: true
200+
flows:
201+
- self check input $task=check_harmful
202+
prompts:
203+
- task: check_harmful
204+
content: |
205+
User message: "{{ user_input }}"
206+
Answer No.
207+
- task: self_check_input
208+
content: |
209+
User message: "{{ user_input }}"
210+
Answer Yes.
211+
enable_rails_exceptions: True
212+
""",
213+
)
214+
215+
rails = LLMRails(config, verbose=False)
216+
response = rails.generate(messages=[{"role": "user", "content": "hello"}])
217+
218+
assert _response_content(response) == "Hey!"
219+
assert check_harmful_llm.inference_count == 1
220+
assert default_input_llm.inference_count == 0
221+
222+
223+
@patch("nemoguardrails.rails.llm.llmrails.init_llm_model")
224+
def test_llmrails_openai_parallel_output_model_routes_by_task(mock_init_llm_model):
225+
main_llm = FakeLLMModel(responses=[" ask question", " Here is the answer."])
226+
check_data_leakage_llm = FakeLLMModel(responses=["No"])
227+
default_output_llm = FakeLLMModel(responses=["Yes"])
228+
mock_init_llm_model.side_effect = [main_llm, check_data_leakage_llm, default_output_llm]
229+
config = RailsConfig.from_content(
230+
colang_content="""
231+
define user ask question
232+
"tell me something"
233+
234+
define flow
235+
user ask question
236+
bot respond
237+
""",
238+
yaml_content="""
239+
models:
240+
- type: main
241+
engine: openai
242+
model: gpt-4o-mini
243+
- type: check_data_leakage
244+
engine: openai
245+
model: gpt-4o-mini
246+
- type: self_check_output
247+
engine: openai
248+
model: gpt-4o-mini
249+
rails:
250+
output:
251+
parallel: true
252+
flows:
253+
- self check output $task=check_data_leakage
254+
prompts:
255+
- task: check_data_leakage
256+
content: |
257+
Bot response: "{{ bot_response }}"
258+
Answer No.
259+
- task: self_check_output
260+
content: |
261+
Bot response: "{{ bot_response }}"
262+
Answer Yes.
263+
enable_rails_exceptions: True
264+
""",
265+
)
266+
267+
rails = LLMRails(config, verbose=False)
268+
response = rails.generate(messages=[{"role": "user", "content": "tell me something"}])
269+
270+
assert _response_content(response) == "Here is the answer."
271+
assert check_data_leakage_llm.inference_count == 1
272+
assert default_output_llm.inference_count == 0
273+
274+
275+
@pytest.mark.asyncio
276+
@patch("nemoguardrails.rails.llm.llmrails.init_llm_model")
277+
async def test_llmrails_openai_streaming_output_rail_routes_custom_task(mock_init_llm_model):
278+
main_llm = FakeLLMModel(
279+
responses=[
280+
' express greeting\nbot express greeting\n "Hi, how are you doing?"',
281+
"This response should stream safely.",
282+
]
283+
)
284+
check_data_leakage_llm = FakeLLMModel(responses=["No", "No"])
285+
default_output_llm = FakeLLMModel(responses=["Yes"])
286+
mock_init_llm_model.side_effect = [main_llm, check_data_leakage_llm, default_output_llm]
287+
config = RailsConfig.from_content(
288+
colang_content="""
289+
define user express greeting
290+
"hi"
291+
292+
define flow
293+
user express greeting
294+
bot tell joke
295+
""",
296+
yaml_content="""
297+
models:
298+
- type: main
299+
engine: openai
300+
model: gpt-4o-mini
301+
- type: check_data_leakage
302+
engine: openai
303+
model: gpt-4o-mini
304+
- type: self_check_output
305+
engine: openai
306+
model: gpt-4o-mini
307+
rails:
308+
output:
309+
flows:
310+
- self check output $task=check_data_leakage
311+
streaming:
312+
enabled: true
313+
chunk_size: 4
314+
context_size: 2
315+
stream_first: false
316+
prompts:
317+
- task: check_data_leakage
318+
content: |
319+
Bot response: "{{ bot_response }}"
320+
Answer No.
321+
- task: self_check_output
322+
content: |
323+
Bot response: "{{ bot_response }}"
324+
Answer Yes.
325+
""",
326+
)
327+
328+
rails = LLMRails(config, verbose=False)
329+
chunks = []
330+
async for chunk in rails.stream_async(messages=[{"role": "user", "content": "hi"}]):
331+
chunks.append(chunk)
332+
333+
assert "This response should stream safely." in "".join(chunks)
334+
assert check_data_leakage_llm.inference_count > 0
335+
assert default_output_llm.inference_count == 0
336+
337+
338+
@pytest.mark.skipif(
339+
not (LIVE_TEST_MODE and os.environ.get("OPENAI_API_KEY") and check_optional_dependency("langchain_openai")),
340+
reason="Requires LIVE_TEST_MODE or TEST_LIVE_MODE, OPENAI_API_KEY, and langchain_openai",
341+
)
342+
def test_llmrails_live_openai_default_self_check_smoke():
343+
config = RailsConfig.from_content(
344+
colang_content=COLANG,
345+
yaml_content=OPENAI_MODEL
346+
+ """
347+
rails:
348+
input:
349+
flows:
350+
- self check input
351+
prompts:
352+
- task: self_check_input
353+
content: |
354+
Always answer exactly No.
355+
""",
356+
)
357+
358+
rails = LLMRails(config, verbose=False)
359+
response = rails.generate(messages=[{"role": "user", "content": "hello"}])
360+
361+
assert response["role"] == "assistant"

0 commit comments

Comments
 (0)