Chain with LUIS Intents - c#

So they have this nice example inside the EchoBot sample to demonstrate chains
public static readonly IDialog<string> dialog = Chain.PostToChain()
.Select(msg => msg.Text)
.Switch(
new Case<string, IDialog<string>>(text =>
{
var regex = new Regex("^reset");
return regex.Match(text).Success;
}, (context, txt) =>
{
return Chain.From(() => new PromptDialog.PromptConfirm("Are you sure you want to reset the count?",
"Didn't get that!", 3)).ContinueWith<bool, string>(async (ctx, res) =>
{
string reply;
if (await res)
{
ctx.UserData.SetValue("count", 0);
reply = "Reset count.";
}
else
{
reply = "Did not reset count.";
}
return Chain.Return(reply);
});
}),
new RegexCase<IDialog<string>>(new Regex("^help", RegexOptions.IgnoreCase), (context, txt) =>
{
return Chain.Return("I am a simple echo dialog with a counter! Reset my counter by typing \"reset\"!");
}),
new DefaultCase<string, IDialog<string>>((context, txt) =>
{
int count;
context.UserData.TryGetValue("count", out count);
context.UserData.SetValue("count", ++count);
string reply = string.Format("{0}: You said {1}", count, txt);
return Chain.Return(reply);
}))
.Unwrap()
.PostToUser();
}
However instead of using a REGEX to determine my conversation path I would much rather use a LUIS Intent. I'm using this nice piece of code to extract the LUIS Intent.
public static async Task<LUISQuery> ParseUserInput(string strInput)
{
string strRet = string.Empty;
string strEscaped = Uri.EscapeDataString(strInput);
using (var client = new HttpClient())
{
string uri = Constants.Keys.LUISQueryUrl + strEscaped;
HttpResponseMessage msg = await client.GetAsync(uri);
if (msg.IsSuccessStatusCode)
{
var jsonResponse = await msg.Content.ReadAsStringAsync();
var _Data = JsonConvert.DeserializeObject<LUISQuery>(jsonResponse);
return _Data;
}
}
return null;
}
Now unfortunately because this is async it doesn't play well with the LINQ query to run the case statement. Can anyone provide me some code that will allow me to have a case statement inside my chain based on LUIS Intents?

Omg is right in his comment.
Remember that IDialogs have a TYPE, meaning, IDialog can return an object of a type specified by yourself:
public class TodoItemDialog : IDialog<TodoItem>
{
// Somewhere, you'll call this to end the dialog
public async Task FinishAsync(IDialogContext context, IMessageActivity activity)
{
var todoItem = _itemRepository.GetItemByTitle(activity.Text);
context.Done(todoItem);
}
}
the call to context.Done() returns the object that your Dialog was meant to return. Whever you're reading a class declaration for any kind of IDialog
public class TodoItemDialog : LuisDialog<TodoItem>
It helps to read it as:
"TodoItemDialog is a Dialog class that returns a TodoItem when it's done"
Instead of chaining, you can use context.Forward() which basically forwards the same messageActivity to another dialog class..
The difference between context.Forward() and context.Call() is essencially that context.Forward() allows you to forward a messageActivity that is immediately handled by the dialog called, while context.Call() simply starts a new dialog, without handing over anything.
From your "Root" dialog, if you need to use LUIS to determine an intent and return a specific object, you can simply forward the messageActivity to it by using Forward and then handling the result in the specified callback:
await context.Forward(new TodoItemDialog(), AfterTodoItemDialogAsync, messageActivity, CancellationToken.None);
private async Task AfterTodoItemDialogAsync(IDialogContext context, IAwaitable<TodoItem> result)
{
var receivedTodoItem = await result;
// Continue conversation
}
And finally, your LuisDialog class could look something like this:
[Serializable, LuisModel("[ModelID]", "[SubscriptionKey]")]
public class TodoItemDialog : LuisDialog<TodoItem>
{
[LuisIntent("GetTodoItem")]
public async Task GetTodoItem(IDialogContext context, LuisResult result)
{
await context.PostAsync("Working on it, give me a moment...");
result.TryFindEntity("TodoItemText", out EntityRecommendation entity);
if(entity.Score > 0.9)
{
var todoItem = _todoItemRepository.GetByText(entity.Entity);
context.Done(todoItem);
}
}
}
(For brevity, I have no ELSE statements in the example, which is something you of course need to add)

Related

Form Flow bot customization issue

I want to build a bot which can make use of QnA api and google drive's search api. I will ask user if he wants to query Knowledge base or he wants to search a file in drive. For this, I chose Form Flow bot template of Bot Framework. In this case, if user chooses to query qna api then I want to post the question to QNA api. How can I implement this in my bot? Where can I find user's selection in flow.
Here is MessageController.cs
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, MakeRootDialog);
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
private Activity HandleSystemMessage(Activity message)
{
if (message.Type == ActivityTypes.DeleteUserData)
{
// Implement user deletion here
// If we handle user deletion, return a real message
}
else if (message.Type == ActivityTypes.ConversationUpdate)
{
// Handle conversation state changes, like members being added and removed
// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
// Not available in all channels
}
else if (message.Type == ActivityTypes.ContactRelationUpdate)
{
// Handle add/remove from contact lists
// Activity.From + Activity.Action represent what happened
}
else if (message.Type == ActivityTypes.Typing)
{
// Handle knowing tha the user is typing
}
else if (message.Type == ActivityTypes.Ping)
{
}
return null;
}
internal static IDialog<UserIntent> MakeRootDialog()
{
return Chain.From(() => FormDialog.FromForm(UserIntent.BuildForm));
}
Form Builder
public static IForm<UserIntent> BuildForm()
{
return new FormBuilder<UserIntent>()
.Message("Welcome to the bot!")
.OnCompletion(async (context, profileForm) =>
{
await context.PostAsync("Thank you");
}).Build();
}
FormFlow is more for a guided conversation flow. It doesn't seem to me like it meets your requirements. You can just use a PromptDialog to get the user's answer for which type of search they prefer, then forward the next message to the corresponding dialog. Something like:
[Serializable]
public class RootDialog : IDialog<object>
{
const string QnAMakerOption = "QnA Maker";
const string GoogleDriveOption = "Google Drive";
const string QueryTypeDataKey = "QueryType";
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
if(context.UserData.ContainsKey(QueryTypeDataKey))
{
var userChoice = context.UserData.GetValue<string>(QueryTypeDataKey);
if(userChoice == QnAMakerOption)
await context.Forward(new QnAMakerDialog(), ResumeAfterQnaMakerSearch, activity);
else
await context.Forward(new GoogleDialog(), ResumeAfterGoogleSearch, activity);
}
else
{
PromptDialog.Choice(
context: context,
resume: ChoiceReceivedAsync,
options: new[] { QnAMakerOption, GoogleDriveOption },
prompt: "Hi. How would you like to perform the search?",
retry: "That is not an option. Please try again.",
promptStyle: PromptStyle.Auto
);
}
}
private Task ResumeAfterGoogleSearch(IDialogContext context, IAwaitable<object> result)
{
//do something after the google search dialog finishes
return Task.CompletedTask;
}
private Task ResumeAfterQnaMakerSearch(IDialogContext context, IAwaitable<object> result)
{
//do something after the qnamaker dialog finishes
return Task.CompletedTask;
}
private async Task ChoiceReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var userChoice = await result;
context.UserData.SetValue(QueryTypeDataKey, userChoice);
await context.PostAsync($"Okay, your preferred search is {userChoice}. What would you like to search for?");
}
}
First you need to create your LUIS application over the LUIS portal, add your intents over there with the utterances, for eg:- intent is "KnowlegeBase", inside that you can create as many utterances which will return this intent.
Inside the application, create a LUISDialog class and call it from your BuildForm:-
await context.Forward(new MyLuisDialog(), ResumeDialog, activity, CancellationToken.None);
LuisDialog class:-
public class MyLuisDialog : LuisDialog<object>
{
private static ILuisService GetLuisService()
{
var modelId = //Luis modelID;
var subscriptionKey = //Luis subscription key
var staging = //whether point to staging or production LUIS
var luisModel = new LuisModelAttribute(modelId, subscriptionKey) { Staging = staging };
return new LuisService(luisModel);
}
public MyLuisDialog() : base(GetLuisService())
{
}
[LuisIntent("KnowledgeBase")]
public async Task KnowledgeAPICall(IDialogContext context, LuisResult result)
{
//your code
}
To utilize the message passed by user, you can use it from the context in the parameter.

Getting result from Func<Task<T>>

Method Under Test
protected override async Task<Name> DoExecuteAsync(NameContext context)
{
context.ThrowIfNull("context");
var request = new Request
{
Id = context.Id,
Principal = context.UserPrincipal,
};
return await this.repository.NameAsync(request, new CancellationToken(), context.ControllerContext.CreateLoggingContext());
}
protected override Name HandleError(NameContext viewContext, Exception exception)
{
if (this.errorSignaller != null)
{
this.errorSignaller.SignalFromCurrentContext(exception);
}
return Name.Unknown;
}
This is implementation of
public abstract class BaseQueryAsync<TInput, TOutput> : IQueryAsync<TInput, TOutput>
{
public async Task<TOutput> ExecuteAsync(TInput context)
{
try
{
return await this.DoExecuteAsync(context);
}
catch (Exception e)
{
return this.HandleError(context, e);
}
}
protected abstract Task<TOutput> DoExecuteAsync(TInput context);
protected virtual TOutput HandleError(TInput viewContext, Exception exception)
{
ExceptionDispatchInfo.Capture(exception).Throw();
}
}
Test Case goes like below
[SetUp]
public void Setup()
{
var httpContext = MvcMockHelpers.MockHttpContext(isAuthenticated: true);
this.controller = new Mock<Controller>();
this.controller.Object.SetMockControllerContext(httpContext.Object);
this.repoMock = new Mock<IRepository>();
this.errorSignaller = new Mock<IErrorSignaller>();
this.query = new NameQuery(this.repoMock.Object, this.errorSignaller.Object);
this.userPrinciple = new Mock<IPrincipal>();
this.context = new NameContext(this.controller.Object.ControllerContext, this.userPrinciple.Object);
}
[Test]
public async Task TestDoExecuteAsyncWhenRepositoryFails()
{
// Arrange
this.repoMock.Setup(
x => x.NameAsync(
It.IsAny<Request>(),
It.IsAny<CancellationToken>(),
It.IsAny<ILoggingContext>())).Throws<Exception>();
// Act
Func<Task<Name>> act = async () => await this.query.ExecuteAsync(this.context);
// Assert
act.ShouldNotThrow();
this.errorSignaller.Verify(s => s.SignalFromCurrentContext(It.IsAny<Exception>()), Times.Once);
}
To verify the Name Object ,When I use the var result = await act() before the line
this.errorSignaller.Verify(s => s.SignalFromCurrentContext(It.IsAny<Exception>()), Times.Once);
The this.errorSignaller.Verify fails since it's count is 2 instead of 1. My intention is to check the Name object along with below code.
act.ShouldNotThrow();
this.errorSignaller.Verify(s => s.SignalFromCurrentContext(It.IsAny<Exception>()), Times.Once);
I knew that if I write a new test case I can easily verify it, but is there any way I can do altogether in this test?
If you want to test the result then use:
Name result = await this.query.ExecuteAsync(this.context);
result.Should().Be(expectefResult);
Make sure to make your test method public async Task
Update
To be able to verify name you would need to set it in the function.
//...code removed for brevity
Name expectedName = Name.Unknown;
Name actualName = null;
// Act
Func<Task> act = async () => {
actualName = await this.query.ExecuteAsync(this.context);
};
// Assert
act.ShouldNotThrow();
actualName
.Should().NotBeNull()
.And.Be(expectedName);
//...rest of code
Original
As already mentioned in the comments, act is a function that returns a Task.
While its implementation is awaited, the function itself still needs to be invoked. And since the function returns a Task it too would need to be awaited.
Func<Task<Name>> act = async () => await this.query.ExecuteAsync(this.context);
var name = await act();
It is the same as having the following function.
async Task<Name> act() {
return await this.query.ExecuteAsync(this.context);
}
You would have to await it the same way
var name = await act();
The only difference being that the former example has the function in a delegate.
Try to avoid mixing blocking calls like .Result with async/await code. This tends to cause deadlocks.
You can try to check it with
await query.ExecuteAsync(this.context);
or
this.query.ExecuteAsync(this.context).GetAwaiter().GetResult();
and in case of Func:
act.Invoke().GetAwaiter().GetResult();

How can you quit from dialog in C# bot-framework?

I'm starting a project for a ChatBot in C# with the bot framework.
I choose the ContosoFlowers example for learning about the use of bot framework. In the AddressDialog users cannot quit the dialog after they enter to it without providing an address.
How can I update the code so when users reply "Cancel" or "Abort" or "B" or "Back" they quit the dialog?
namespace ContosoFlowers.BotAssets.Dialogs
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Extensions;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Properties;
using Services;
[Serializable]
public class AddressDialog : IDialog<string>
{
private readonly string prompt;
private readonly ILocationService locationService;
private string currentAddress;
public AddressDialog(string prompt, ILocationService locationService)
{
this.prompt = prompt;
this.locationService = locationService;
}
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync(this.prompt);
context.Wait(this.MessageReceivedAsync);
}
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
var addresses = await this.locationService.ParseAddressAsync(message.Text);
if (addresses.Count() == 0)
{
await context.PostAsync(Resources.AddressDialog_EnterAddressAgain);
context.Wait(this.MessageReceivedAsync);
}
else if (addresses.Count() == 1)
{
this.currentAddress = addresses.First();
PromptDialog.Choice(context, this.AfterAddressChoice, new[] { Resources.AddressDialog_Confirm, Resources.AddressDialog_Edit }, this.currentAddress);
}
else
{
var reply = context.MakeMessage();
reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;
foreach (var address in addresses)
{
reply.AddHeroCard(Resources.AddressDialog_DidYouMean, address, new[] { new KeyValuePair<string, string>(Resources.AddressDialog_UseThisAddress, address) });
}
await context.PostAsync(reply);
context.Wait(this.MessageReceivedAsync);
}
}
private async Task AfterAddressChoice(IDialogContext context, IAwaitable<string> result)
{
try
{
var choice = await result;
if (choice == Resources.AddressDialog_Edit)
{
await this.StartAsync(context);
}
else
{
context.Done(this.currentAddress);
}
}
catch (TooManyAttemptsException)
{
throw;
}
}
}
}
You just need to handle the quit conditions at the top of the MessageReceivedAsync method.
At the top of the dialog you can add something like
private static IEnumerable<string> cancelTerms = new[] { "Cancel", "Back", "B", "Abort" };
And also add this method:
public static bool IsCancel(string text)
{
return cancelTerms.Any(t => string.Equals(t, text, StringComparison.CurrentCultureIgnoreCase));
}
Then is just a matter of understanding if the message sent by the user matches any of the cancelation terms. In the MessageReceivedAsync method do something like:
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if (IsCancel(message.Text))
{
context.Done<string>(null);
}
// rest of the code of this method..
}
You can also go a bit more generic and create a CancelableIDialog similar to what was done in the CancelablePromptChoice.

LuisDialog not working anymore after update to 1.2.0.1

I've updated my NuGet packages to use the version 1.2.0.1 of the Microsoft Bot Framework.
Some breaking changes were reported here, and I managed to fix the build errors. But the application is not working anymore..
I have two problems:
The code throws an InvalidIntentHandlerException when I send a message an utterance to the controller.
In my 'intent' method (decorated with the LuisIntent attribute) it was possible to read the value of the entities. Like so:
[Serializable]
[LuisModel("xxxxx", "xxxx")]
public class BookFlightDialog : LuisDialog<BookFlightForm>
{
private readonly BuildFormDelegate<BookFlightForm> BuildForm;
internal BookFlightDialog(BuildFormDelegate<BookFlightForm> buildForm)
{
BuildForm = buildForm;
}
[LuisIntent("")]
[LuisIntent("None")]
public async Task None(IDialogContext context, LuisResult result)
{
await context.PostAsync("I'm sorry. I didn't understand you.");
context.Wait(MessageReceived);
}
[LuisIntent("BookAFlight")]
public async Task BookAFlight(IDialogContext context, LuisResult result)
{
var form = new BookFlightForm();
// var entities = new List<EntityRecommendation>(result.Entities);
var locations = result.Entities.Where(e => e.Type.Equals("builtin.geography") || e.Type.Equals("builtin.geography.city")).OrderBy(e => e.StartIndex);
if (locations.Any())
{
form.LocationFrom = locations.First().Name;
if (locations.Count() == 2)
{
form.LocationTo = locations.Skip(1).First().Name;
}
}
var date = result.Entities.FirstOrDefault(e => e.Type == "builtin.datetime.date");
if (date != null) form.DepartureDate = DateTime.Parse(date.Name);
var formDialog = new FormDialog<BookFlightForm>(form, BuildForm, FormOptions.PromptInStart);
context.Call(formDialog, OnComplete);
}
private async Task OnComplete(IDialogContext context, IAwaitable<BookFlightForm> result)
{
BookFlightForm booking;
try
{
booking = await result;
}
catch (OperationCanceledException)
{
await context.PostAsync("Ok, see you later.");
return;
}
if (booking != null)
{
var service = new SkyScannerService();
var possibilities = await service.Search(booking);
await context.PostAsync(possibilities);
}
else
{
await context.PostAsync("Form returned empty response!");
}
context.Wait(MessageReceived);
}
}
How do I fix the exception and how do I read the value of the entities?
Thanks once again!
This is because you are not using inbuilt LuisResult class by having using LuisResult = Bots.Results.LuisResult;. Replace it with using Microsoft.Bot.Builder.Luis.Models;.

ObjectDisposedException when trying to access Thread.CurrentPrincipal with async method

I'm fairly new to the new async/await stuff. However, I have the following classes:
public abstract class PluginBase
{
//Using EF to store log info to database
private EFContext _context = new EFContext();
private int Id = 1;
protected void LogEvent(string event, string details)
{
_context.LogEvents.Add(new LogItem(){
PluginId = this.Id,
Event = event,
Details = details,
User = Thread.CurrentPrincipal.Identity.Name
});
}
}
public class Plugin : PluginBase
{
public void Process()
{
CallWebService();
}
public async void CallWebService()
{
using(var http = new HttpClient())
{
...
var result = await http.PostAsync(memberURI, new StringContent(content, Encoding.UTF8,"application/json"));
if(result.IsSuccessStatusCode)
_status = "Success";
else
_status = "Fail";
LogEvent("Service Call - " + _status,...);
}
}
So, the idea is that Plugin.Process gets called. It in turn calls CallWebService(). CallWebService makes an asynchronous call to http.PostAsync. When I return from that call and try to call base.LogEvent(), I get an ObjectDisposedException stating that "Safe Handle has been Closed".
I know there is something going on where when the awaitable finishes, the rest of the code of the method has to be run. Maybe its being run in some other thread or context? If this is the case, how do I get the current user at the time of writing to the log?
Thanks for your help with this.
Edit
Based on the answer from Yuval, I made the following changes and it seems to work fine.
public void Process()
{
var task = CallWebService();
task.Wait();
}
public async Task CallWebService(List<Member> members)
{
using(var http = new HttpClient())
{
...
using(var result = await http.PostAsync(memberURI, new StringContent content, Encoding.UTF8, "application/json")))
{
if(result.IsSuccessStatusCode)
_status = "Success";
else
_status = "Fail";
LogEvent("Service Call - " + _status,...);
}
}
}
When I return from that call and try to call base.LogEvent(), I get an
ObjectDisposedException stating that "Safe Handle has been Closed".
That's because somewhere higher up the call-chain, someone is disposing your plugin object, which hasn't really finished the asynchronous operation. Using async void is doing a "fire and forget" operation. You don't actually await on Process, hence anyone calling it assumes it finished and disposes of your object.
Change your async void method to async Task, and await it:
public Task ProcessAsync()
{
return CallWebServiceAsync();
}
public async Task CallWebServiceAsync()
{
using (var http = new HttpClient())
{
var result = await http.PostAsync(memberURI,
new StringContent(content,
Encoding.UTF8,
"application/json"));
if (result.IsSuccessStatusCode)
_status = "Success";
else
_status = "Fail";
LogEvent("Service Call - " + _status,...);
}
}
Note you will need to await ProcessAsync somewhere higher up the call-stack as well.

Categories

Resources