Stopping Logic
By default, ConversationSimulator ends a simulation when the expected_outcome in your ConversationalGolden has been met. You can replace this behavior with a custom stopping_controller that returns proceed() or end().
from deepeval.simulator import ConversationSimulator
from deepeval.simulator.controller import end, proceed
async def stopping_controller(last_assistant_turn, simulated_user_turns):
if last_assistant_turn and "confirmation number" in last_assistant_turn.content.lower():
return end(reason="User received a confirmation number")
return proceed()
simulator = ConversationSimulator(
model_callback=model_callback,
stopping_controller=stopping_controller,
)Function name in your code can be anything — the example uses stopping_controller for parity but def my_stop(...) works equally well.
Stopping Order
A ConversationSimulator run can end for four different reasons. They are checked in a fixed order on every iteration of the simulation loop:
max_user_simulations— hard outer cap. Checked first.stopping_controller— your custom (or the defaultexpected_outcome) gate. Runs before the next user turn is generated.SimulationNode.max_visitsexhausted — fires from inside the simulation graph runner when a node would emit its(N+1)-th turn; the runner skips emission and ends.SimulationNode(terminal=True)— fires after the user turn from a terminal node and the matching assistant reply have both been recorded.
The sequence diagram below traces one iteration and highlights where each terminator can fire:
The sequence diagram below traces one happy-path iteration and marks where each terminator can fire:
The four terminators are complementary, not redundant — see the comparison in the Simulation Graph docs for when to reach for which.
Supported Arguments
Only define the arguments your callback needs. deepeval will pass supported arguments by name:
- [Optional]
turns: the current list ofTurns in the simulation. - [Optional]
golden: theConversationalGoldenbeing simulated. - [Optional]
index: the index of the turn being simulated. - [Optional]
thread_id: the unique thread ID for the simulated conversation. - [Optional]
simulated_user_turns: the number of new simulated user turns generated so far. - [Optional]
max_user_simulations: the maximum number of user-assistant message cycles allowed. - [Optional]
last_user_turn: the latest userTurn, if one exists. - [Optional]
last_assistant_turn: the latest assistantTurn, if one exists.
Return Values
If your callback returns anything other than proceed() or end(), deepeval treats it the same as proceed(). This is useful when you only want to explicitly handle terminal states:
import random
from deepeval.simulator.controller import end, proceed
def stopping_controller():
if random.random() > 0.5:
return end(reason="Random early stop")
return proceed()Your callback can return:
proceed(): continue the simulation.end(reason=...): end the simulation and optionally record why.- Anything else, including
None: continue the simulation.
Common Use Cases
Confirmation States
Many task flows should stop as soon as your chatbot confirms the user completed the task.
from deepeval.simulator.controller import end, proceed
def stopping_controller(last_assistant_turn):
if last_assistant_turn and "confirmation number" in last_assistant_turn.content.lower():
return end(reason="User received confirmation")
return proceed()Tool Completion
When your chatbot returns tool call metadata, a specific successful tool call can be the clearest completion signal.
from deepeval.simulator.controller import end, proceed
def stopping_controller(last_assistant_turn):
if last_assistant_turn and any(
tool.name == "issue_refund"
for tool in last_assistant_turn.tools_called or []
):
return end(reason="Refund tool was called")
return proceed()Repeated Failures
For unhelpful simulations where the assistant repeatedly fails, end early instead of letting them run to the max-turn cap.
from deepeval.simulator.controller import end, proceed
def stopping_controller(turns):
assistant_turns = [turn for turn in turns if turn.role == "assistant"]
recent = assistant_turns[-2:]
if len(recent) == 2 and all("I don't know" in turn.content for turn in recent):
return end(reason="Assistant failed twice in a row")
return proceed()