@@ -255,48 +255,89 @@ def test_create_isolated_llms_for_actions_integration(self, rails_with_mock_llm)
255255 """Test the full isolated LLM creation process."""
256256 rails = rails_with_mock_llm
257257
258+ # Mock rails configuration with flows
259+ rails .config .rails = Mock ()
260+ rails .config .rails .input = Mock ()
261+ rails .config .rails .output = Mock ()
262+ rails .config .rails .input .flows = ["input_flow_1" , "input_flow_2" ]
263+ rails .config .rails .output .flows = ["output_flow_1" ]
264+
258265 rails .runtime = Mock ()
259266 rails .runtime .action_dispatcher = MockActionDispatcher ()
260267 rails .runtime .registered_action_params = {}
261268 rails .runtime .register_action_param = Mock ()
262269
263- rails ._create_isolated_llms_for_actions ()
264-
265- expected_calls = [
270+ # Mock get_action_details_from_flow_id to return actions that need LLMs
271+ def mock_get_action_details (flow_id , flows ):
272+ mapping = {
273+ "input_flow_1" : ("action_with_llm" , {}),
274+ "input_flow_2" : ("generate_user_intent" , {}),
275+ "output_flow_1" : ("self_check_output" , {}),
276+ }
277+ return mapping .get (flow_id , ("unknown_action" , {}))
278+
279+ with patch (
280+ "nemoguardrails.rails.llm.llmrails.get_action_details_from_flow_id" ,
281+ side_effect = mock_get_action_details ,
282+ ):
283+ rails ._create_isolated_llms_for_actions ()
284+
285+ expected_llm_params = [
266286 "action_with_llm_llm" ,
267287 "generate_user_intent_llm" ,
268288 "self_check_output_llm" ,
269289 ]
270290
271- actual_calls = [
291+ registered_llm_params = [
272292 call [0 ][0 ] for call in rails .runtime .register_action_param .call_args_list
273293 ]
274294
275- for expected_call in expected_calls :
276- assert expected_call in actual_calls
295+ for expected_param in expected_llm_params :
296+ assert expected_param in registered_llm_params
277297
278298 def test_create_isolated_llms_skips_existing_specialized_llms (
279299 self , rails_with_mock_llm
280300 ):
281301 """Test that existing specialized LLMs are not overridden."""
282302 rails = rails_with_mock_llm
283303
304+ # Mock rails configuration with flows
305+ rails .config .rails = Mock ()
306+ rails .config .rails .input = Mock ()
307+ rails .config .rails .output = Mock ()
308+ rails .config .rails .input .flows = ["input_flow_1" , "input_flow_2" ]
309+ rails .config .rails .output .flows = ["output_flow_1" ]
310+
284311 rails .runtime = Mock ()
285312 rails .runtime .action_dispatcher = MockActionDispatcher ()
286313 rails .runtime .registered_action_params = {"self_check_output_llm" : Mock ()}
287314 rails .runtime .register_action_param = Mock ()
288315
289- rails ._create_isolated_llms_for_actions ()
290-
291- # verify self_check_output_llm was NOT re-registered
292- actual_calls = [
316+ # Mock get_action_details_from_flow_id to return actions that need LLMs
317+ def mock_get_action_details (flow_id , flows ):
318+ mapping = {
319+ "input_flow_1" : ("action_with_llm" , {}),
320+ "input_flow_2" : ("generate_user_intent" , {}),
321+ "output_flow_1" : (
322+ "self_check_output" ,
323+ {},
324+ ), # This one already has an LLM
325+ }
326+ return mapping .get (flow_id , ("unknown_action" , {}))
327+
328+ with patch (
329+ "nemoguardrails.rails.llm.llmrails.get_action_details_from_flow_id" ,
330+ side_effect = mock_get_action_details ,
331+ ):
332+ rails ._create_isolated_llms_for_actions ()
333+
334+ registered_llm_params = [
293335 call [0 ][0 ] for call in rails .runtime .register_action_param .call_args_list
294336 ]
295- assert "self_check_output_llm" not in actual_calls
296337
297- # but other actions should still get isolated LLMs
298- assert "action_with_llm_llm" in actual_calls
299- assert "generate_user_intent_llm" in actual_calls
338+ assert "self_check_output_llm" not in registered_llm_params
339+ assert "action_with_llm_llm" in registered_llm_params
340+ assert "generate_user_intent_llm" in registered_llm_params
300341
301342 def test_create_isolated_llms_handles_no_main_llm (self , mock_config ):
302343 """Test graceful handling when no main LLM is available."""
@@ -411,3 +452,87 @@ def test_action_detection_parametrized(
411452 assert action_name in actions_needing_llms
412453 else :
413454 assert action_name not in actions_needing_llms
455+
456+ def test_create_isolated_llms_for_configured_actions_only (
457+ self , rails_with_mock_llm
458+ ):
459+ """Test that isolated LLMs are created only for actions configured in rails flows."""
460+ rails = rails_with_mock_llm
461+
462+ rails .config .rails = Mock ()
463+ rails .config .rails .input = Mock ()
464+ rails .config .rails .output = Mock ()
465+ rails .config .rails .input .flows = [
466+ "input_flow_1" ,
467+ "input_flow_2" ,
468+ "input_flow_3" ,
469+ ]
470+ rails .config .rails .output .flows = ["output_flow_1" , "output_flow_2" ]
471+
472+ rails .runtime = Mock ()
473+ rails .runtime .action_dispatcher = MockActionDispatcher ()
474+ rails .runtime .registered_action_params = {}
475+ rails .runtime .register_action_param = Mock ()
476+
477+ def mock_get_action_details (flow_id , flows ):
478+ mapping = {
479+ "input_flow_1" : ("action_with_llm" , {}),
480+ "input_flow_2" : ("action_without_llm" , {}),
481+ "input_flow_3" : ("self_check_output" , {}),
482+ "output_flow_1" : ("generate_user_intent" , {}),
483+ "output_flow_2" : ("non_configured_action" , {}),
484+ }
485+ return mapping .get (flow_id , ("unknown_action" , {}))
486+
487+ with patch (
488+ "nemoguardrails.rails.llm.llmrails.get_action_details_from_flow_id" ,
489+ side_effect = mock_get_action_details ,
490+ ):
491+ rails ._create_isolated_llms_for_actions ()
492+
493+ registered_llm_params = [
494+ call [0 ][0 ] for call in rails .runtime .register_action_param .call_args_list
495+ ]
496+
497+ expected_isolated_llm_params = [
498+ "action_with_llm_llm" ,
499+ "generate_user_intent_llm" ,
500+ "self_check_output_llm" ,
501+ ]
502+
503+ for expected_param in expected_isolated_llm_params :
504+ assert (
505+ expected_param in registered_llm_params
506+ ), f"Expected { expected_param } to be registered as action param"
507+
508+ assert "action_without_llm_llm" not in registered_llm_params
509+ assert "non_configured_action_llm" not in registered_llm_params
510+
511+ assert len (registered_llm_params ) == 3 , (
512+ f"Should only create isolated LLMs for actions from config flows that need LLMs. "
513+ f"Got { registered_llm_params } "
514+ )
515+
516+ def test_create_isolated_llms_handles_empty_rails_config (self , rails_with_mock_llm ):
517+ """Test that the method handles empty rails configuration gracefully."""
518+ rails = rails_with_mock_llm
519+
520+ rails .config .rails = Mock ()
521+ rails .config .rails .input = Mock ()
522+ rails .config .rails .output = Mock ()
523+ rails .config .rails .input .flows = []
524+ rails .config .rails .output .flows = []
525+
526+ rails .runtime = Mock ()
527+ rails .runtime .action_dispatcher = MockActionDispatcher ()
528+ rails .runtime .registered_action_params = {}
529+ rails .runtime .register_action_param = Mock ()
530+
531+ with patch (
532+ "nemoguardrails.rails.llm.llmrails.get_action_details_from_flow_id"
533+ ) as mock_get_action :
534+ rails ._create_isolated_llms_for_actions ()
535+
536+ mock_get_action .assert_not_called ()
537+
538+ rails .runtime .register_action_param .assert_not_called ()
0 commit comments