Updating an activity in BotFramework after a timeout - c#

I have a requirement that a Bot message posted to MS Teams should expire after a timeout period. To achieve this I am calling UpdateAsyncActivity() on a separate thread after a timeout, however this fails with a NullReferenceException:
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Bot.Builder.BotFrameworkAdapter.UpdateActivityAsync(ITurnContext turnContext, Activity activity, CancellationToken cancellationToken)
at Microsoft.Bot.Builder.TurnContext.<>c__DisplayClass31_0.<<UpdateActivityAsync>g__ActuallyUpdateStuff|0>d.MoveNext()--- End of stack trace from previous location where exception was thrown ---
at Microsoft.Bot.Builder.TurnContext.UpdateActivityInternalAsync(Activity activity, IEnumerable`1 updateHandlers, Func`1 callAtBottom, CancellationToken cancellationToken)
at Microsoft.Bot.Builder.TurnContext.UpdateActivityAsync(IActivity activity, CancellationToken cancellationToken)
at NotifyController.ClearCard(ITurnContext turnContext, Activity timeoutActivity, Int32 timeoutInMinutes) in NotifyController.cs:line 48
The code looks something like this:
[Route("api/notify")]
[ApiController]
public class NotifyController : ControllerBase
{
private IBotFrameworkHttpAdapter Adapter;
private readonly string _appId;
private readonly IConversationStorage _conversationStorage;
public NotifyController(IBotFrameworkHttpAdapter adapter, IConfiguration configuration, IConversationStorage conversationStorage)
{
Adapter = adapter;
_appId = configuration["MicrosoftAppId"] ?? string.Empty;
_conversationStorage = conversationStorage;
}
[HttpPost]
public async Task<IActionResult> PostForm([FromBody] RestData restData)
{
ConversationReference conversationReference =
_conversationStorage.GetConversationFromStorage(restData.ConversationId);
await ((BotAdapter)Adapter).ContinueConversationAsync(_appId, conversationReference, async (context, token) =>
await BotCallback(restData.AdaptiveCard, context, token), default(CancellationToken));
return new ContentResult();
}
private async Task BotCallback(string adaptiveCard, ITurnContext turnContext, CancellationToken cancellationToken)
{
var activity = MessageFactory.Attachment(adaptiveCard.ToAttachment());
ResourceResponse response = await turnContext.SendActivityAsync(activity);
var timeoutActivity = turnContext.Activity.CreateReply();
timeoutActivity.Attachments.Add(AdaptiveCardExamples.TimeoutTryLater.ToAttachment());
timeoutActivity.Id = response.Id;
// SUCCESS
//Thread.Sleep(10000);
//await turnContext.UpdateActivityAsync(timeoutActivity);
// FAIL
Thread CardClearThread = new Thread(() => ClearCard(turnContext, timeoutActivity));
CardClearThread.Start();
}
private async void ClearCard(ITurnContext turnContext, Activity timeoutActivity)
{
Thread.Sleep(10000);
await turnContext.UpdateActivityAsync(timeoutActivity);
}
}
I want the timeout to happen on a separate thread so that PostForm() returns a response as soon as the original message is sent.
I presume what is happening is that some aspect of turnContext is being disposed when the main thread returns, causing the sleeping thread to fail when it wakes up.
Is there a solution/alternative approach to this?

With in some mins , Bot has to response to the team otherwise this issue get popup best solution for this to implement proactive message concept
https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-proactive-message?view=azure-bot-service-4.0&tabs=csharp

Related

Duplicating messages in Teams Channel using Microsoft Bot Framework SDK

everyone.
I have some issues with bot framework SDK. When I pass bot's link to use in MS Teams to another users, they said me there is duplication of messages.
I don't know if is my code. For example. I have a main class:
Bot<T> : ActivityHandler where T : Dialog
Inside this class I use this methods:
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
var activity = turnContext.Activity;
//Using for Adaptive cards.
if (string.IsNullOrWhiteSpace(activity.Text) && activity.Value != null)
{
activity.Text = JsonConvert.SerializeObject(activity.Value);
}
await base.OnTurnAsync(turnContext, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
await _dialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
protected override async Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
await base.OnEndOfConversationActivityAsync(turnContext, cancellationToken);
await _conversationState.DeleteAsync(turnContext, cancellationToken);
}
I never use OnMembersAddedAsync, because my bot is 1 to 1.
Does anyone know how to solve this problem? I test my bot with different users, I tested by myself, but I don't know why I have that unexpected behavior.
Maybe, solve this problem or get a temporary solution.

How to cancel running hangfire job?

This is my codebase. I need to stop running job. I've tried BackgroundJob.Delete method and send current jobId but it didn't help.It just deletes the job but not cancelling it. I can run multiple jobs and should be able to stop each of them from UI. I tried to use CancellationToken but on the UI I am using AJAX to send request and it takes some milliseconds so I can't even abort this request. Can somebody suggest something please? Thanks.
public class JobController : Controller {
public void InternalLogic(int id, CancellationToken cancellationToken)
{
foreach (var item in collection)
{
if (someCondition)
{
if (cancellationToken.IsCancellationRequested)
{
//some logic
break;
}
//continue working
}
}
}
public void RunLogic(int id, CancellationToken cancellationToken)
{
//some logic
InternalLogic(id,cancellationToken);
}
public void Run(int id, CancellationToken cancellationToken)
{
var jobId = BackgroundJob.Enqueue(() => this.RunLogic(id, cancellationToken));
}
}
I'm not sure if I understand your question exactly, but I believe that the CancellationToken you are using is short lived. The request to start the job executes and completes at which point the CancellationToken you are using is useless and will never be cancelled since the request that created it has long since completed successfully and discarded its CancellationTokenSource.
If you want to use CancellationToken, you will need to create your own CancellationTokenSource, keep track of it and cancel the job yourself when you need to cancel it.
Perhaps something similar in concept to:
public class JobController : Controller {
//Not safe, for demonstration purposes only
private static Dictionary<int, CancellationTokenSource> _dictionary = new Dictionary<int, CancellationTokenSource>();
private void InternalLogic(int id, CancellationToken cancellationToken)
{
foreach (var item in collection)
{
if (someCondition)
{
if (cancellationToken.IsCancellationRequested)
{
//some logic
break;
}
//continue working
}
}
}
private void RunLogic(int id, CancellationToken cancellationToken)
{
//some logic
InternalLogic(id,cancellationToken);
}
//Client requests that a job is started
public void Run(int id, CancellationToken cancellationToken)
{
var cts = new CancellationTokenSource()
var jobId = BackgroundJob.Enqueue(() => this.RunLogic(id, cts.Token));
}
//Client requests that a job is cancelled.
public void Cancel(int id, CancellationToken cancellationToken)
{
_dictionary[id].Cancel(); //or perhaps track using jobId?
}
}
This is not a production ready solution - you will want to make sure only the correct user can cancel the correct jobs, and will probably want to find something more elegant and thread safe than a static dictionary.
This solution is not specific to Hangfire - it's just the general idea of how you might signal cancellation of a long running, asynchronous background task.

Swagger Cancel button should trigger CancellationSource

[ApiController]
[Route("[controller]")]
public class JobController : ControllerBase
{
private readonly IEventBus _bus;
public JobController(IUnitOfWork unitOfWork, IEventBus bus)
{
_bus = bus;
}
...
[HttpPost]
public async Task<IActionResult> Post([FromBody]JobRequest request)
{
try
{
var command = new JobCommand{ Id = 1, Name = "abc"};
await _bus.SendCommand(command);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Task was cancelled!");
}
return CreatedAtAction(nameof(GetById), new { id = 0 }, null);
}
}
public class JobCommandHandler : IRequestHandler<JobCommand, bool>
{
private readonly ILogger<JobCommandHandler> _logger;
public JobCommandHandler(ILogger<JobCommandHandler> logger)
{
_logger = logger;
}
public async Task<bool> Handle(JobCommand request, CancellationToken cancellationToken)
{
//
// I was able to reproduce manual cancellation by using this code below
var cts = new CancellationTokenSource();
cts.Cancel();
cancellationToken = cts.Token;
// how this can be populated sent from the place where I'm issuing command?
cancellationToken.ThrowIfCancellationRequested();
...
// long running task
}
}
My question is:
Do I need to send the CancellationTokenSource together with command? If so, how to trigger that from the swagger Cancel button and is it good practice to include CancellationTokenSource
to be the property of CommandBase class which every Command will extend?
If you want to cancel all IO and processing if the Http Request which initiated everything got canceled, then, yes. You have to make everything in its path cancellation aware and pass the token through all layers.
You can get a request cancellation token from the HttpContext in the controller from HttpContext.RequestAborted.
Now, in your example, I'm not sure what IEventBus is. If that is some kind of distributed messaging backend, simply passing a CancellationToken will not work, in that case you could send a cancel event to cancel the action I guess.
If it is in-memory eventing only, then it should probably work.

Access BotState variables from outside Bot class

In the bot classes we create ConversationState and UserState objects that we can access using its accessors and create properties on them, and then save the data we store.
But how could I do the same if I want to access the data from a Dialog that's called from the Bot class? I know I can pass an object through the Context using BeginDialogAsync options parameter. How could I pass two of them instead of just one and how to get them in the dialog class?
Is there a way to access ConversationState and UserState without having to pass them from dialog to dialog?
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occured during the turn.
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Running dialog with Message Activity.");
// Run the Dialog with the new message Activity.
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
In this function of the CoreBot sample we can see the ConversationState and UserState are saved but they are not modified anywhere else, and in the second function a DialogState property is created in the child dialog but it isn't used either that I can see? Can somebody explain why are they created and how to access them from inside the Dialog that is just called?
You can use dependency injection.
public class UserStateClass
{
public string name { get; set; }
}
and
public class YourDialog : ComponentDialog
{
private readonly IStatePropertyAccessor<UserStateClass> _userStateclassAccessor;
public YourDialog(UserState userState)
: base(nameof(YourDialog))
{
_userProfileAccessor = userState.CreateProperty<UserStateClass>("UserProfile");
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
}
private async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var userstate = await _userStateclassAccessor.GetAsync(stepContext.Context, () => new UserStateClass(), cancellationToken);
userstate.name = "pepe";
return await stepContext.EndDialogAsync();
}
}

textprompt after button click in adaptive card doesnt invoke next step

Functionality:
Fetch and display relevant answer from QnAMaker
This response must be followed by an adaptive card with checkboxes
Based on the values selected, invoke a web service to provide the user with required file links.
Display a feedback message 'Did this help?'
Direct the conversation flow based on user response.
Problem Statement: For the step 2 mentioned above, the button click is being handled at the bot.cs file and redirected to a new Dialog.
Everything works fine until displaying the feedback message(which is again invoked from a new Dialog). However, after this text prompt the
next step is not called and exits with an error: The given key 'dialogs' was not present in the dictionary.
Why does it show that error without going to the next step?
bot.cs
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext.Activity.Type == ActivityTypes.Message)
{
if (turnContext.Activity.Text != null)
{
if (!dc.Context.Responded)
{
// examine results from active dialog
switch (dialogResult.Status)
{
case DialogTurnStatus.Empty:
switch (topIntent)
{
case ...
}
break;
case ...
}
}
}
else if (string.IsNullOrEmpty(turnContext.Activity.Text))
{
await HandleSubmitActionAsync(turnContext, userProfile);
}
}
}
private async Task HandleSubmitActionAsync(ITurnContext turnContext, UserProfile userProfile)
{
if (value.Type == "GetCredentials")
{
userProfile.credentialsCard = true;
}
await dc.BeginDialogAsync(nameof(HandleButtonDialog));
}
HandleButtonDialog:
public HandleButtonDialog(BotServices _services, UserProfile _userProfile) : base(Name)
{
botServices = _services ?? throw new ArgumentNullException(nameof(_services));
userProfile = _userProfile;
var waterfallSteps = new WaterfallStep[]
{
GetFeedbackStepAsync,
FeedbackStepAsync,
FeedbackResponseStepAsync,
};
AddDialog(new WaterfallDialog(HBFeedbackDialog));
AddDialog(new TextPrompt("userFeed"));
}
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
if...
else if(userProfile.credentialsCard == true)
{
await dc.BeginDialogAsync(HandleCredentialsFeedbackDialog.Name);
}
}
HandleCredentialsFeedbackDialog:
public class HandleCredentialsFeedbackDialog : ComponentDialog
{
public HandleCredentialsFeedbackDialog(BotServices services, UserProfile _userProfile,string dialogId = null) : base(Name)
{
botServices = services ?? throw new ArgumentNullException(nameof(services));
userProfile = _userProfile;
// This array defines how the Waterfall will execute.
var waterfallSteps = new WaterfallStep[]
{
CredsValidate,
GetFeedbackStepAsync,
FeedbackStepAsync,
FeedbackResponseStepAsync,
};
AddDialog(new TextPrompt("userFeed"));
AddDialog(new WaterfallDialog(HBFeedbackDialog, waterfallSteps));
InitialDialogId = HBFeedbackDialog;
}
public async Task<DialogTurnResult> CredsValidate(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
VALIDATE THE CHECKBOX SELECTED VALUES
....
//Invoke Web Service
var qnaReuslt = await MakeBatchRequestCreds(stepContext.Context, finalSearchList.ToArray());
return await stepContext.PromptAsync("userFeed", new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply("Did this help?")
});
}
}
Error Stack Trace:
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at Microsoft.Bot.Builder.Dialogs.ComponentDialog.RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\ComponentDialog.cs:line 112
at Microsoft.Bot.Builder.Dialogs.ComponentDialog.ResumeDialogAsync(DialogContext outerDc, DialogReason reason, Object result, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\ComponentDialog.cs:line 106
at Microsoft.Bot.Builder.Dialogs.DialogContext.EndDialogAsync(Object result, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\DialogContext.cs:line 196
at AESAiLean.Dialogs.HandleButtonDialog.BeginDialogAsync(DialogContext dc, Object options, CancellationToken cancellationToken) in C:\Users\...\Dialogs\HandleButtonDialog.cs:line 199
at Microsoft.Bot.Builder.Dialogs.DialogContext.BeginDialogAsync(String dialogId, Object options, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder.Dialogs\DialogContext.cs:line 113
at AESAiLean.AESAiLeanBot.HandleSubmitActionAsync(ITurnContext turnContext, UserProfile userProfile) in C:\Users\...\Bots\AESAiLeanBot.cs:line 361

Categories

Resources