Skip to content

Commit d35f3d3

Browse files
author
wayflow-bot
committed
Merge branch 'fix-agent-no-user-infinite-loop' into 'main'
Fix Infinite Loop in CallerInputMode.NEVER See merge request wayflow-dev/wayflow-public!202
2 parents b790ebb + dc3250d commit d35f3d3

File tree

4 files changed

+189
-6
lines changed

4 files changed

+189
-6
lines changed

wayflowcore/src/wayflowcore/agent.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ class CallerInputMode(Enum):
3838
ALWAYS = "always" # doc: The agent is allowed to ask the user for more information if needed
3939

4040

41+
NOT_SET_INITIAL_MESSAGE = "N/A"
4142
DEFAULT_INITIAL_MESSAGE = "Hi! How can I help you?"
4243

4344

4445
@dataclass
4546
class Agent(ConversationalComponent, SerializableDataclassMixin, SerializableObject):
4647

48+
NOT_SET_INITIAL_MESSAGE: ClassVar[str] = NOT_SET_INITIAL_MESSAGE
49+
"""str: Placeholder for non-explicitly set initial message."""
50+
4751
DEFAULT_INITIAL_MESSAGE: ClassVar[str] = DEFAULT_INITIAL_MESSAGE
4852
"""str: Message the agent will post if no previous user message to welcome them."""
4953

@@ -111,7 +115,7 @@ def __init__(
111115
max_iterations: int = 10,
112116
context_providers: Optional[List["ContextProvider"]] = None,
113117
can_finish_conversation: bool = False,
114-
initial_message: Optional[str] = DEFAULT_INITIAL_MESSAGE,
118+
initial_message: Optional[str] = NOT_SET_INITIAL_MESSAGE,
115119
caller_input_mode: CallerInputMode = CallerInputMode.ALWAYS,
116120
input_descriptors: Optional[List["Property"]] = None,
117121
output_descriptors: Optional[List["Property"]] = None,
@@ -185,9 +189,9 @@ def __init__(
185189
can_finish_conversation
186190
Whether the agent can decide to end the conversation or not.
187191
initial_message:
188-
Initial message the agent will post if no previous user message.
189-
Default to ``Agent.DEFAULT_INITIAL_MESSAGE``. If None, the LLM will generate it but the agent requires
190-
a custom_instruction.
192+
Initial message the agent will post if no previous user message. It must be None for `CallerInputMode.NEVER`
193+
If None for `CallerInputMode.ALWAYS`, the LLM will generate it given the `custom_instruction`. Default to
194+
`Agent.DEFAULT_INITIAL_MESSAGE` for `CallerInputMode.ALWAYS` and None for `CallerInputMode.NEVER`.
191195
caller_input_mode:
192196
whether the agent is allowed to ask the user questions (CallerInputMode.ALWAYS) or not (CallerInputMode.NEVER).
193197
If set to NEVER, the agent won't be able to yield.
@@ -253,13 +257,44 @@ def __init__(
253257
flows = [_convert_described_flow_into_named_flow(flow) for flow in flows]
254258
self._validate_flow_can_be_used_in_composition(flows)
255259

260+
if caller_input_mode not in [CallerInputMode.ALWAYS, CallerInputMode.NEVER]:
261+
raise ValueError(
262+
f"`caller_input_mode` value {caller_input_mode} is not within the domain of `CallerInputMode`."
263+
)
264+
265+
if initial_message == NOT_SET_INITIAL_MESSAGE:
266+
if caller_input_mode == CallerInputMode.ALWAYS:
267+
initial_message = DEFAULT_INITIAL_MESSAGE
268+
elif caller_input_mode == CallerInputMode.NEVER:
269+
initial_message = None
270+
else:
271+
raise NotImplementedError(
272+
f"The case of not explicitly setting the `initial message` with caller_input_mode = {caller_input_mode} is not supported. This is probably a problem from our side. Please try to proceed with explicitly setting a value for `initial_message`."
273+
)
274+
256275
if output_descriptors is not None and len(
257276
set(descriptor.name for descriptor in output_descriptors)
258277
) < len(output_descriptors):
259278
raise ValueError(
260279
f"Detected name conflicts in outputs of the Agent. Please ensure output names are unique: {output_descriptors}"
261280
)
262281

282+
if caller_input_mode == CallerInputMode.NEVER and initial_message is not None:
283+
raise ValueError(
284+
"The caller input mode for the agent is set to `CallerInputMode.NEVER`, which does not allow setting an initial message."
285+
)
286+
287+
if (
288+
caller_input_mode == CallerInputMode.NEVER
289+
and tools is not None
290+
and len(tools) > 0
291+
and max_iterations <= 1
292+
):
293+
warnings.warn(
294+
"Maximum number of iterations is set to one for the Agent. The agent will be not able to call the tools and report the result.",
295+
UserWarning,
296+
)
297+
263298
self.llm = llm
264299

265300
self.tools = _convert_previously_supported_tools_if_needed(tools) or []

wayflowcore/tests/integration/steps/test_agentexecution_step.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def test_agent_executor_step_can_read_previous_messages(
256256
agent = Agent(
257257
llm=remotely_hosted_llm,
258258
caller_input_mode=CallerInputMode.NEVER,
259+
custom_instruction="Please help me with my tasks.",
259260
_filter_messages_by_recipient=False,
260261
)
261262
output_step = OutputMessageStep(

wayflowcore/tests/integration/test_agent.py

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from wayflowcore.models.llmmodel import LlmModel
3030
from wayflowcore.models.llmmodelfactory import LlmModelFactory
3131
from wayflowcore.models.vllmmodel import VllmModel
32-
from wayflowcore.property import IntegerProperty, StringProperty
32+
from wayflowcore.property import BooleanProperty, IntegerProperty, StringProperty
3333
from wayflowcore.steps import (
3434
AgentExecutionStep,
3535
InputMessageStep,
@@ -1618,3 +1618,145 @@ async def test_agent_can_run_async(remotely_hosted_llm):
16181618
conversation = agent.start_conversation()
16191619
status = await conversation.execute_async()
16201620
assert isinstance(status, UserMessageRequestStatus)
1621+
1622+
1623+
def test_error_on_caller_input_mode_never_with_initial_message(big_llama):
1624+
with pytest.raises(
1625+
ValueError, match="The caller input mode for the agent is set to `CallerInputMode.NEVER`"
1626+
):
1627+
Agent(
1628+
llm=big_llama,
1629+
caller_input_mode=CallerInputMode.NEVER,
1630+
initial_message="Hi, what's your name?",
1631+
)
1632+
1633+
1634+
def _get_haiku_tool(
1635+
submitted_haikus: List[str],
1636+
success_message: str = "Haiku Submitted Successfully.",
1637+
):
1638+
@tool(description_mode="only_docstring")
1639+
def submit_haiku(haiku: str) -> str:
1640+
"""Submit your haiku.
1641+
1642+
Parameters
1643+
----------
1644+
haiku :
1645+
the full haiku (all three verses of it)
1646+
1647+
Returns
1648+
-------
1649+
A status code
1650+
"""
1651+
submitted_haikus.append(haiku)
1652+
return success_message
1653+
1654+
return submit_haiku
1655+
1656+
1657+
@retry_test(max_attempts=3)
1658+
@pytest.mark.parametrize("can_finish_conversation", [True])
1659+
def test_caller_input_mode_never(big_llama, can_finish_conversation):
1660+
"""
1661+
Failure rate: 0 out of 20
1662+
Observed on: 2025-07-28
1663+
Average success time: 4.20 seconds per successful attempt
1664+
Average failure time: No time measurement
1665+
Max attempt: 3
1666+
Justification: (0.05 ** 3) ~= 9.4 / 100'000
1667+
"""
1668+
1669+
submitted_haikus = []
1670+
1671+
agent = Agent(
1672+
llm=big_llama,
1673+
tools=[_get_haiku_tool(submitted_haikus)],
1674+
caller_input_mode=CallerInputMode.NEVER,
1675+
custom_instruction="You are a helpful assistant, who always uses the appropriate tool to submit a single Haiku, and then finishes the conversation.",
1676+
initial_message=None,
1677+
can_finish_conversation=can_finish_conversation,
1678+
max_iterations=5,
1679+
)
1680+
1681+
conv = agent.start_conversation()
1682+
conv.execute()
1683+
assert len(submitted_haikus) == 1
1684+
1685+
1686+
@retry_test(max_attempts=3)
1687+
@pytest.mark.parametrize("can_finish_conversation", [True, False])
1688+
def test_caller_input_mode_never_with_agent_template(big_llama, can_finish_conversation):
1689+
"""
1690+
Failure rate: 0 out of 20
1691+
Observed on: 2025-07-28
1692+
Average success time: 4.20 seconds per successful attempt
1693+
Average failure time: No time measurement
1694+
Max attempt: 3
1695+
Justification: (0.05 ** 3) ~= 9.4 / 100'000
1696+
"""
1697+
submitted_haikus = []
1698+
1699+
agent = Agent(
1700+
llm=big_llama,
1701+
tools=[_get_haiku_tool(submitted_haikus)],
1702+
caller_input_mode=CallerInputMode.NEVER,
1703+
initial_message=None,
1704+
custom_instruction="You are a helpful assistant, who always uses the appropriate tool to submit a single Haiku, and then finishes the conversation.",
1705+
agent_template=PromptTemplate(
1706+
messages=[
1707+
Message("{{user_input}}", MessageType.USER),
1708+
PromptTemplate.CHAT_HISTORY_PLACEHOLDER,
1709+
],
1710+
),
1711+
output_descriptors=[
1712+
BooleanProperty(
1713+
"haiku_submitted",
1714+
description="true if the haiku was successfully submitted",
1715+
default_value=False,
1716+
)
1717+
],
1718+
can_finish_conversation=can_finish_conversation,
1719+
max_iterations=5,
1720+
)
1721+
1722+
conv = agent.start_conversation(inputs={"user_input": "I want my haiku to be about trees"})
1723+
status = conv.execute()
1724+
assert isinstance(status, FinishedStatus)
1725+
assert status.output_values["haiku_submitted"]
1726+
assert len(submitted_haikus) == 1
1727+
1728+
1729+
@retry_test(max_attempts=3)
1730+
def test_caller_input_mode_never_with_single_iteration(big_llama):
1731+
"""
1732+
Failure rate: 0 out of 20
1733+
Observed on: 2025-07-28
1734+
Average success time: 0.87 seconds per successful attempt
1735+
Average failure time: No time measurement
1736+
Max attempt: 3
1737+
Justification: (0.05 ** 3) ~= 9.4 / 100'000
1738+
"""
1739+
submitted_haikus = []
1740+
1741+
with pytest.warns(
1742+
UserWarning, match="Maximum number of iterations is set to one for the Agent.*"
1743+
):
1744+
agent = Agent(
1745+
llm=big_llama,
1746+
tools=[_get_haiku_tool(submitted_haikus)],
1747+
caller_input_mode=CallerInputMode.NEVER,
1748+
custom_instruction="You are a helpful assistant, who always uses the appropriate tool to submit a single Haiku, and then finishes the conversation.",
1749+
output_descriptors=[
1750+
BooleanProperty(
1751+
"haiku_submitted", "true if the haiku was submitted", default_value=False
1752+
)
1753+
],
1754+
initial_message=None,
1755+
can_finish_conversation=False,
1756+
max_iterations=1,
1757+
)
1758+
1759+
conv = agent.start_conversation()
1760+
status = conv.execute()
1761+
assert isinstance(status, FinishedStatus)
1762+
assert not status.output_values["haiku_submitted"]

wayflowcore/tests/test_caller_input_mode.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,9 @@ def test_agent_raises_when_passing_talk_to_user_tool_when_callerinputmode_is_set
182182
ValueError,
183183
match=f"Caller input mode is set to 'NEVER' but found a tool with name {_TALK_TO_USER_TOOL_NAME}. Make sure to not pass any tool with this name.",
184184
):
185-
_ = Agent(llm=llm, tools=[talk_to_user_tool], caller_input_mode=CallerInputMode.NEVER)
185+
_ = Agent(
186+
llm=llm,
187+
tools=[talk_to_user_tool],
188+
caller_input_mode=CallerInputMode.NEVER,
189+
custom_instruction="A random custom instruction. If custom instruction is not provided, an earlier exception will appear.",
190+
)

0 commit comments

Comments
 (0)