Reputation: 10208
I've been reading around the docs and looking at code samples for hints and inspiration and so far I've turned up nothing.
If we catch a global exception occuring in our bots we enter the OnTurnError
handler:
// Catches any errors that occur during a conversation turn and logs them.
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
};
I haven't found anything in the docs or in any discussions where anyone is subsequently recovering from an error and restarting the conversation. I have a solution working but I'm wondering if I'm missing a more "best practice" method. I'm just doing this at the moment:
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
await _conversationState.DeleteAsync(context);
await MyBot.SendIntroCardAsync(context, CancellationToken.None);
};
Without some kind of recovery like this we are leaving users stranded in dead conversations. Is there a better solution I'm not finding?
Upvotes: 4
Views: 675
Reputation: 11
I'm using the Python version of the SDK and what worked for me was the on_turn method
: Olace a try-except
and after except
to clear conversation:
async def on_turn(self, turn_context: TurnContext):
try:
await super().on_turn(turn_context)
if self.state_profile_accessor.get(turn_context, CustomState) is not None:
state_profile = await self.state_profile_accessor.get(turn_context, CustomState)
except Exception as e:
await self.conversation_state.clear_state(turn_context)
await self.profile_state.clear_state(turn_context)
# await self.profile_state.delete(turn_context)
# await self.conversation_state.profile_state.delete(turn_context)
time.sleep(2)
message = MessageFactory.attachment("resetting")
await turn_context.send_activity(message)
await self.conversation_state.save_changes(turn_context)
await self.profile_state.save_changes(turn_context)
Anyway I'm using a my own Custom State, but the .clear_state & .save_changes restarts it ok for my use-case. Hope this helps.
Upvotes: 1
Reputation: 33379
"Wiping" Conversation State and starting over is certainly one way to handle it, albeit a bit heavy handed. Calling DeleteAsync
is the right way to do that though. Perhaps consider that you don't want to wipe all conversation state and maybe just wipe the DialogState
instead? You would do that by just calling IStatePropertyAccessor<DialogState>::DeleteAsync
instead. All depends on what you're maintaining in state and where though.
Now, where I see a problem is in trying to trigger your bot from the OnTurnError
handler to sort of instantly restart the conversation when this happens. It looks like you're calling a static method (SendIntroCardAsync
), which I suppose works, but it's created some very tight coupling that makes me uncomfortable.
Part of me wants to suggest that, if your MyBot
really wants to be so involved in this level of exception handling, then maybe put an actual try
/catch
into the OnTurnAsync
of the bot itself that basically keeps the handling of the activity square in its domain of responsibility and able to cleanly contain and trigger the SendIntroCardAsync
. Then, in that case, the OnTurnError
likely would never be hit unless the bot failed to handle the exception correctly. I would still keep everything in that handler logic except the call to SendIntroCardAsync
, but now you know that's only ever going to be triggered in the scenarios where the bot fails to handle the exception correctly (which should be ultra-rare) or a piece of upstream middleware throws an exception. I don't really love this either though because it puts a responsibility into the bot that it could otherwise be agnostic of.
I think, given that, the final approach I would probably go with would be to build a specific piece of middleware that I would install at the "top" of the pipeline that does top level exception handling and, instead of knowing what exactly to call once it's done that handling, it actually just re-executes the entire pipeline again some configurable number of times. That way your downstream middleware and bot remain "dumb" from a top level exception handling perspective, yet you can still choose what to do (in your case wipe some state) by configuring a callback with this piece of middleware when you register it and, ultimately, your bot will see the replay of the turn once the exception has been handled as a fresh request. This middleware could even add something to the ITurnContext::TurnState
bag that lets downstream logic detect that it's in a replay state and behave slightly differently. For example, imagine your bot being able to do this:
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if(turnContext.IsReplayingTurnBecauseOfException())
{
... send sorry msg and restart dialogs from wherever you want ...
}
}
Where the IsReplayingTurnBecauseOfException
(name up for debate) is an extension method for ITurnContext
that would be provided along with this new piece of middleware to abstract code from reading TurnState
details directly. Something like:
public static bool IsReplayingTurnBecauseOfException(this ITurnContext turnContext) =>
turnContext.TurnState.ContainsKey("MySuperAwesomeExceptionHandlingMiddleware.IsReplayingBecauseOfException");
Upvotes: 4