Skip to content

Commit 04d03ae

Browse files
authored
fix(actions): rename generate_next_step to generate_next_steps for task-specific LLM support (#1603)
Fixes a naming mismatch bug where the action `generate_next_step` (singular) didn't match the Task enum value `generate_next_steps` (plural). This mismatch prevented task-specific LLM configuration from working correctly. When users configured a task-specific LLM with `type: generate_next_steps` in their config, the runtime would look for `generate_next_step_llm` (based on the action name) but the config registered `generate_next_steps_llm`, causing the task to fall back to the main LLM instead.
1 parent e999078 commit 04d03ae

File tree

7 files changed

+100
-8
lines changed

7 files changed

+100
-8
lines changed

nemoguardrails/actions/llm/generation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ async def _search_flows_index(self, text, max_results):
599599
return final_results[0:max_results]
600600

601601
@action(is_system_action=True)
602-
async def generate_next_step(self, events: List[dict], llm: Optional[BaseLLM] = None):
602+
async def generate_next_steps(self, events: List[dict], llm: Optional[BaseLLM] = None):
603603
"""Generate the next step in the current conversation flow.
604604
605605
Currently, only generates a next step after a user intent.

nemoguardrails/llm/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def co_v2(
8181
"GenerateFlowFromInstructionsAction",
8282
"GenerateFlowFromNameAction",
8383
"generate_intent_steps_message",
84-
"generate_next_step",
84+
"generate_next_steps",
8585
"GenerateUserIntentAction",
8686
"GenerateValueAction",
8787
"GetLastUserMessageAction",

nemoguardrails/rails/llm/llm_flows.co

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ define flow generate next step
7878
priority 0.9
7979

8080
user ...
81-
execute generate_next_step
81+
execute generate_next_steps
8282

8383

8484
define parallel extension flow generate bot message

tests/test_configs/with_actions_override/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async def generate_user_intent():
2525

2626

2727
@action(is_system_action=True)
28-
async def generate_next_step():
28+
async def generate_next_steps():
2929
return ActionResult(events=[{"type": "BotIntent", "intent": "respond to question"}])
3030

3131

@@ -38,5 +38,5 @@ async def generate_bot_message():
3838

3939
def init(app: LLMRails):
4040
app.register_action(generate_user_intent)
41-
app.register_action(generate_next_step)
41+
app.register_action(generate_next_steps)
4242
app.register_action(generate_bot_message)

tests/test_general_instructions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ async def test_search_flows_index_is_none():
158158

159159

160160
@pytest.mark.asyncio
161-
async def test_generate_next_step_empty_event_list():
161+
async def test_generate_next_steps_empty_event_list():
162162
"""Check if we try and search the flows index when None we get None back"""
163163

164164
config = RailsConfig(
@@ -175,7 +175,7 @@ async def test_generate_next_step_empty_event_list():
175175
)
176176

177177
with pytest.raises(RuntimeError, match="No last user intent found from which to generate next step"):
178-
_ = await actions.generate_next_step(events=[])
178+
_ = await actions.generate_next_steps(events=[])
179179

180180

181181
#

tests/test_task_specific_model.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
18+
import pytest
19+
20+
from nemoguardrails import LLMRails, RailsConfig
21+
from nemoguardrails.imports import check_optional_dependency
22+
from nemoguardrails.rails.llm.options import GenerationResponse
23+
24+
has_langchain_openai = check_optional_dependency("langchain_openai")
25+
26+
has_openai_key = bool(os.getenv("OPENAI_API_KEY"))
27+
28+
skip_if_no_openai = pytest.mark.skipif(
29+
not (has_langchain_openai and has_openai_key),
30+
reason="Requires langchain_openai and OPENAI_API_KEY environment variable",
31+
)
32+
33+
34+
@skip_if_no_openai
35+
def test_task_specific_model_for_generate_user_intent_and_generate_next_steps():
36+
config = RailsConfig.from_content(
37+
colang_content="""
38+
define user express greeting
39+
"hi"
40+
41+
define flow
42+
user express greeting
43+
bot express greeting
44+
45+
define bot express greeting
46+
"Hello! How can I assist you today?"
47+
""",
48+
yaml_content="""
49+
models:
50+
- type: main
51+
engine: openai
52+
model: gpt-3.5-turbo-instruct
53+
54+
- type: generate_user_intent
55+
engine: openai
56+
model: gpt-4o-mini
57+
58+
- type: generate_next_steps
59+
engine: openai
60+
model: gpt-4o-mini
61+
""",
62+
)
63+
64+
rails = LLMRails(config)
65+
66+
res = rails.generate(
67+
messages=[{"role": "user", "content": "what can you do?"}],
68+
options={"log": {"llm_calls": True}},
69+
)
70+
71+
assert isinstance(res, GenerationResponse)
72+
assert res.log is not None
73+
assert res.log.llm_calls is not None
74+
assert len(res.log.llm_calls) > 0
75+
76+
task_specific_tasks = ["generate_user_intent", "generate_next_steps"]
77+
78+
generate_user_intent_calls = [call for call in res.log.llm_calls if call.task == "generate_user_intent"]
79+
assert len(generate_user_intent_calls) > 0
80+
for call in generate_user_intent_calls:
81+
assert call.llm_model_name == "gpt-4o-mini"
82+
assert call.llm_provider_name == "openai"
83+
84+
generate_next_steps_calls = [call for call in res.log.llm_calls if call.task == "generate_next_steps"]
85+
assert len(generate_next_steps_calls) > 0
86+
for call in generate_next_steps_calls:
87+
assert call.llm_model_name == "gpt-4o-mini"
88+
assert call.llm_provider_name == "openai"
89+
90+
other_calls = [call for call in res.log.llm_calls if call.task not in task_specific_tasks]
91+
for call in other_calls:
92+
assert call.llm_model_name == "gpt-3.5-turbo-instruct"

tests/tracing/spans/test_span_v2_otel_semantics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_llm_span_has_complete_attributes(self):
121121

122122
rail = ActivatedRail(
123123
type="dialog",
124-
name="generate_next_step",
124+
name="generate_next_steps",
125125
started_at=1.0,
126126
finished_at=2.0,
127127
duration=1.0,

0 commit comments

Comments
 (0)