In part 1 we laid the ground work for Kylie bot and through this post we will extend this functionality to start doing some meaningful things. To get started, we should have a look at the project that we have just created and understand its structure.
From the project structure, we will focus on three main areas:
1. Controllers
Controllers are used to manage the flow of messages between the user and the bot. Primarily they are used to evaluate whether or not a message is a system message (like someone leaving the conversation) or a user message.When the controller establishes that it is a user message we will then typically follow a Dialog.
2. Dialogs
Dialogs allow for a structured conversation to be held between the bot and the user. These Dialogs come in a variety of forms and most of our custom logic will sit in here. Through this blog series we will come to look at a number of different Dialogs and passing context back and forward between them.
3. Models
Models are the representation of the objects we intend to use throughout the application. For this example, I have created a User model which will mimic some of the common attributes of a user that would interact with a bot. My User class looks as follows:
namespace KylieBot.Models { [Serializable] public class User { public string Name { get; set; }
public string Id { get; set; }
public string Token { get; set; }
public bool WantsToBeAuthenticated { get; set; } } }
Not much is going on here but we do have the ability to store both the Id and the Name of the user before they are authenticated. We also have an indicator of whether or not the user wants to authenticate with the bot.
Now that we have a User model established, let's modify our Controller to make use of it. By default we only have one controller called MessagesController.cs.
From the structure of the controller, you will notice that we have the option to process all messages posted to the controller as follows:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { await Conversation.SendAsync(activity, () => new RootDialog(u)); } else { await HandleSystemMessageAsync(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
This is the code that handles whether or not the bot has received a message from the user. We are going to look at the HandleSystemMessageAsync method we we will be able to pick up that a user has been added to the conversation. To do this, we are going to look at the handler for the message type of ConversationUpdate. Here, the conversation will be seen to be updated when a user joins the conversation.
Some important parts to notice from the method is that an automated reply is being sent each time a user is added when the user is the default user conversing with the bot. The automated reply also includes some markup such as the double asterisk (**) which when used around a word or phrase, will render it in bold. Also, the (\n\n) which represents a new paragraph. This markup renders differently depending on the channel that you are interacting with the bot on, so do test this thoroughly!
Two branches also exist to handle specific users being added or removed from the conversation by inserting a message into the conversation so that there is awareness of the addition/removal of a user.
There is also a class level variable that is established to represent the default User. this object then gets populated with the details that we have available to use and passed to the RootDialog once the user initiates contact with the bot. Code for the MessagesController.cs file is as follows:
namespace KylieBot { [BotAuthentication] public class MessagesController : ApiController { User u = new Models.User(); public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { if (activity.Type == ActivityTypes.Message) { await Conversation.SendAsync(activity, () => new RootDialog(u)); } else { await HandleSystemMessageAsync(activity); } var response = Request.CreateResponse(HttpStatusCode.OK); return response; }
private async Task<Activity> HandleSystemMessageAsync(Activity message) { if (message.Type == ActivityTypes.DeleteUserData) { } else if (message.Type == ActivityTypes.ConversationUpdate) { ConnectorClient connector = new ConnectorClient(new Uri(message.ServiceUrl)); List<User> memberList = new List<User>();
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { var client = scope.Resolve<IConnectorClient>(); var activityMembers = await client.Conversations.GetConversationMembersAsync(message.Conversation.Id);
foreach (var member in activityMembers) { memberList.Add(new User() { Id = member.Id, Name = member.Name }); }
if (message.MembersAdded != null && message.MembersAdded.Any(o => o.Id == message.Recipient.Id)) { u.Id = message.From.Id; u.Name = message.From.Name; var intro = message.CreateReply("Hello **" + message.From.Name + "**! I am **Kylie Bot (KB)**. \n\n What can I assist you with?"); await connector.Conversations.ReplyToActivityAsync(intro); } }
if (message.MembersAdded != null && message.MembersAdded.Any() && memberList.Count > 2) { var added = message.CreateReply(message.MembersAdded[0].Name + " joined the conversation"); await connector.Conversations.ReplyToActivityAsync(added); }
if (message.MembersRemoved != null && message.MembersRemoved.Any()) { var removed = message.CreateReply(message.MembersRemoved[0].Name + " left the conversation"); await connector.Conversations.ReplyToActivityAsync(removed); } } else if (message.Type == ActivityTypes.ContactRelationUpdate) { } else if (message.Type == ActivityTypes.Typing) { } else if (message.Type == ActivityTypes.Ping) { Activity reply = message.CreateReply(); reply.Type = ActivityTypes.Ping; return reply; } return null; } } }
Now that we have identified our user, and have prompted them for some input ("What can I assist you with?"), we will wait for the user to post a message and then handle it via our RootDialog. This dialog will be passed our user object so that we can enhance it with an authentication token, if we get one.
We are going to user Azure Active Directory to authenticate our users and this will be achieved through registering an App in the directory. To do so, click here and complete the App registration process with the credentials and directory of your choice (these don't have to be the same credentials that we used in part 1).
You will be presented with a landing page like this:
Sign in and then you will be presented with a quick start page which you can skip by clicking the top right option to do so.
Next, you will be able to register you App with the necessary details. Remember to make a note of the Application Id and Password as we will need these shortly. If a password has not been generated for you automatically, you may need to manually create one by clicking on the 'Generate New Password' button. You can leave all other options in their default setting as this is providing the minimum permission we will need. It is important to include a redirect URL and this can be set to: "http://localhost:3979/api/OAuthCallback"
Now that we have our App registered, we can move back over to Visual Studio and right click on our Kylie Bot project and select Manage Nuget Packages as below:
We are going to use a pre-packaged authentication mechanism to speed things along. In order to do so, we will need to check the Include Prerelease checkbox and then search for 'authbot'. We should be presented with a package from Mat Velloso. We can go ahead and install this in our project.
Once this is done, we can navigate to our RootDialog class and implement the functionality. To do this, we are firstly going to check if we have a token and if we do, we are just going to wait for another message from the user. If we don't have a token, we are going to forward our request to the package we have just installed and this will handle the authentication process for us. The token will then be returned to us and we will update our User object with it. Finally, we will include an option for the user to logout if they already have a token and wish to do so. The code for the RootDialog class looks like this:
namespace KylieBot.Dialogs { [Serializable] public class RootDialog : IDialog<object> { private User user;
public RootDialog(User user) { this.user = user; }
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 (string.IsNullOrEmpty(await context.GetAccessToken(AuthSettings.Scopes))) { await context.Forward(new AzureAuthDialog(AuthSettings.Scopes), this.ResumeAfterAuth, activity, CancellationToken.None); } else { context.Wait(MessageReceivedAsync); }
if (!string.IsNullOrEmpty(user.Token) && activity.Text == "logout") { await context.Logout(); } }
private async Task ResumeAfterAuth(IDialogContext context, IAwaitable<string> result) { var message = await result; user.Token = await context.GetAccessToken(AuthSettings.Scopes); await context.PostAsync(message); context.Wait(MessageReceivedAsync); } } }
The second from last bit that we need to update is our web.config file. This will have been pre-populated with some keys that are necessary for the authentication to complete. This is where you will update the Application Id and Password fields with the data from your own App that you registered earlier.Your web.config should look similar to this:
Finally, the last step of the process is to make the bot aware of the keys. To do this, the following needs to be added in the Global.asax file:
AuthBot.Models.AuthSettings.Mode = ConfigurationManager.AppSettings["ActiveDirectory.Mode"];
AuthBot.Models.AuthSettings.EndpointUrl = ConfigurationManager.AppSettings["ActiveDirectory.EndpointUrl"];
AuthBot.Models.AuthSettings.Tenant = ConfigurationManager.AppSettings["ActiveDirectory.Tenant"];
AuthBot.Models.AuthSettings.RedirectUrl = ConfigurationManager.AppSettings["ActiveDirectory.RedirectUrl"];
AuthBot.Models.AuthSettings.ClientId = ConfigurationManager.AppSettings["ActiveDirectory.ClientId"];
AuthBot.Models.AuthSettings.ClientSecret = ConfigurationManager.AppSettings["ActiveDirectory.ClientSecret"];
AuthBot.Models.AuthSettings.Scopes = ConfigurationManager.AppSettings["ActiveDirectory.Scopes"].Split(',');
We should be able to test out bot with our new authentication mechanism. To do so, run the Visual Studio project and then run the emulator and connect the emulator to the bot as we did in part 1. This time you will notice that you are greeted by Kylie Bot before being asked to input something that she can assist with. Once you input a message, you will be prompted to authenticate as follows:
The authentication process will launch an external webpage that will ask the user to login with their Microsoft\Live credentials like this:
Next, the user will be asked ton confirm that the Kyle Bot is authorised to read some of their profile information as follows:
Once, confirmed, the user will then be presented a unique token number that will need to be sent to the bot to complete the authentication process as follows:
The bot will then respond with a personalised welcome message based on the profile attached to the credentials that the user logged in with!
While this may still seem simple, the premise is that we now have an authentication token form a Microsoft Identity Provider that we can then further use to access knowledge sources such as Partner Source and the Dynamics Learning Portal!
Look out for some further work around this in part 3!
The complete code for the solution can obtained by registering here (it's free) and then downloading the solution.
**Don't forget to check in your code!