In the part 2 of the series, we have discussed on the concept of how the Bot Framework library is used to assist us building conversational bot. As promised in the previous post, I’m going to share the source code on how the bot framework can be integrated with Dynamics CRM.
The Story
Now, to build a conversational bot, it will begin with the use case of the bot. In my example, I’m going to use an imaginary car dealer with this simple “User Story” from Scrum principle: “As a customer, I want to be able chat and let the company know that I want to test drive a car, so that I can make an informed decision when I’m buying the car”.
The Implementation
In the previous post, we have discussed the concept of Dialog, Form Flow and Luis Dialog. Now in this post, I will show on how these concept can be applied.
In general I’m creating the TestDriveDetail class to contain the test drive request detail:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace CrmChatBot.Model | |
{ | |
[Serializable] | |
public class TestDriveDetail | |
{ | |
public string CarMake { get; set; } | |
public string CarModel { get; set; } | |
public string RequestedTime { get; set; } | |
public string CustomerName { get; set; } | |
public string PhoneNumber { get; set; } | |
} | |
} |
Notice that all classes that will be used in Bot Framework will need to be decorated with [Serializable].
And a simple helper class to create the record to CRM.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void CreateTestDrive(TestDriveDetail testDrive, IOrganizationService crmService) | |
{ | |
var lead = new Microsoft.Xrm.Sdk.Entity(EntityName); | |
//lead.Attributes | |
lead.Attributes.Add(Field_Subject, $"Test Drive Request by {testDrive.CustomerName}"); | |
lead.Attributes.Add(Field_FirstName, testDrive.CustomerName); | |
lead.Attributes.Add(Field_Description, $@"Test drive request summary: | |
{Environment.NewLine}Car Make: {testDrive.CarMake}, | |
{Environment.NewLine}Car Model: {testDrive.CarModel}, | |
{Environment.NewLine}Requested Time: {testDrive.RequestedTime}, | |
{Environment.NewLine}Customer Name: {testDrive.CustomerName}, | |
{Environment.NewLine}Phone Number: {testDrive.PhoneNumber}"); | |
crmService.Create(lead); | |
} |
Now, I’ll give the example of the simple implementation with the 3 different techniques (Dialog, Form Flow and Luis Dialog).
Sample #1: Simple Dialog
The sample dialog is a series of prompt and at the end of the process it the store the information in CRM Online. Below is sample code of the Dialog, how the chain of prompts are created and at the end it is storing the record in CRM.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using System.Web; | |
using Microsoft.Bot.Builder.Dialogs; | |
using Microsoft.Bot.Connector; | |
using System.Text.RegularExpressions; | |
using CrmChatBot.Model; | |
using CrmChatBot.CRM; | |
namespace CrmChatBot.Dialogs | |
{ | |
[Serializable] | |
public class CarInquiryDialog :IDialog<object> | |
{ | |
protected TestDriveDetail testDriveDetail; | |
public async Task StartAsync(IDialogContext context) | |
{ | |
context.Wait(MessageReceivedAsync); | |
} | |
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument) | |
{ | |
var message = await argument; | |
//CrmDataConnection.GetAPI(); | |
if (message.Text.Contains("test drive")) | |
{ | |
testDriveDetail = new TestDriveDetail(); | |
PromptDialog.Text( | |
context: context, | |
resume: CarMakeHandler, | |
prompt: "What car make do you want to test?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
else if (message.Text == "No") | |
{ | |
} | |
else | |
{ | |
await context.PostAsync("Hi there, anything that I can help for you today?"); | |
context.Wait(MessageReceivedAsync); | |
} | |
} | |
public virtual async Task CarMakeHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var carMake = await argument; | |
testDriveDetail.CarMake = carMake; | |
PromptDialog.Text( | |
context: context, | |
resume: CarModelHandler, | |
prompt: "What car model do you want to test?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task CarModelHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var carModel = await argument; | |
testDriveDetail.CarModel = carModel; | |
PromptDialog.Text( | |
context: context, | |
resume: PreferredTimeHandler, | |
prompt: "When would you like to come for test drive?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task PreferredTimeHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var prefTime = await argument; | |
testDriveDetail.RequestedTime = prefTime; | |
PromptDialog.Text( | |
context: context, | |
resume: CustomerNameHandler, | |
prompt: "Your name please?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task CustomerNameHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var customerName = await argument; | |
testDriveDetail.CustomerName = customerName; | |
PromptDialog.Text( | |
context: context, | |
resume: ContactNumberHandler, | |
prompt: "What is the best number to contact you?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task ContactNumberHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var contactNumber = await argument; | |
testDriveDetail.PhoneNumber = contactNumber; | |
await context.PostAsync($@"Thank you for your interest, your request has been logged. Our sales team will get back to you shortly. | |
{Environment.NewLine}Your test drive request summary: | |
{Environment.NewLine}Car Make: {testDriveDetail.CarMake}, | |
{Environment.NewLine}Car Model: {testDriveDetail.CarModel}, | |
{Environment.NewLine}Requested Time: {testDriveDetail.RequestedTime}, | |
{Environment.NewLine}Customer Name: {testDriveDetail.CustomerName}, | |
{Environment.NewLine}Phone Number: {testDriveDetail.PhoneNumber}"); | |
//CrmLead.CreateTestDrive(testDriveDetail, CrmDataConnection.GetAPI()); | |
CrmLead.CreateTestDrive(testDriveDetail, CrmDataConnection.GetOrgService()); | |
context.Done<string>("Test drive has been logged"); | |
} | |
} | |
} |
Sample #2: Form Flow
As you can see at the above screen, the form flow is automatically generate the questions with the pre-defined options. Below is the source code of Form Flow implementation:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using CrmChatBot.CRM; | |
using CrmChatBot.Model; | |
using Microsoft.Bot.Builder.Dialogs; | |
using Microsoft.Bot.Builder.FormFlow; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web; | |
namespace CrmChatBot.FormFlow | |
{ | |
public enum CarMakeOptions { Unknown, Honda, Toyota }; | |
public enum CarModelOptions { Unknown, Jazz, City, CRV, Accord, HRV, Yaris, Corolla, Camry }; | |
[Serializable] | |
public class CarInquiryFormFlow | |
{ | |
public CarMakeOptions CarMake; | |
public CarModelOptions CarModel; | |
public string PreferredTime; | |
public string Name; | |
public string ContactNumber; | |
public static IForm<CarInquiryFormFlow> BuildForm() | |
{ | |
OnCompletionAsyncDelegate<CarInquiryFormFlow> processRequest = async (context, state) => | |
{ | |
await context.PostAsync($@"Your test drive request summary: | |
{Environment.NewLine}Car Make: {state.CarMake.ToString()}, | |
{Environment.NewLine}Car Model: {state.CarModel.ToString()}, | |
{Environment.NewLine}Requested Time: {state.PreferredTime}, | |
{Environment.NewLine}Customer Name: {state.Name}, | |
{Environment.NewLine}Phone Number: {state.ContactNumber}"); | |
var testDriveDetail = new TestDriveDetail | |
{ | |
CarMake = state.CarMake.ToString(), | |
CarModel = state.CarModel.ToString(), | |
RequestedTime = state.PreferredTime, | |
CustomerName = state.Name, | |
PhoneNumber = state.ContactNumber | |
}; | |
// save the data to CRM | |
CrmLead.CreateTestDrive(testDriveDetail, CrmDataConnection.GetOrgService()); | |
}; | |
return new FormBuilder<CarInquiryFormFlow>() | |
.Message("Welcome to the car test drive bot!") | |
.Field(nameof(CarMake)) | |
.Field(nameof(CarModel)) | |
.Field(nameof(PreferredTime)) | |
.Field(nameof(Name)) | |
.Field(nameof(ContactNumber)) | |
.AddRemainingFields() | |
.Message("Thank you for your interest, your request has been logged. Our sales team will get back to you shortly.") | |
.OnCompletion(processRequest) | |
.Build(); | |
} | |
} | |
} |
To initiate the Form Flow from message controller:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) | |
{ | |
if (activity.Type == ActivityTypes.Message) | |
{ | |
// Initiates the form flow | |
await Conversation.SendAsync(activity, MakeRootDialog); | |
} | |
else | |
{ | |
HandleSystemMessage(activity); | |
} | |
var response = Request.CreateResponse(HttpStatusCode.OK); | |
return response; | |
} | |
internal static IDialog<CarInquiryFormFlow> MakeRootDialog() | |
{ | |
return Chain.From(() => FormDialog.FromForm(CarInquiryFormFlow.BuildForm)); | |
} |
Sample #3: Luis Dialog
Now, we have seen how Dialog and Form Flow is getting the simple conversation started. However, if you might notice, the bot can only understand predefined keywords or options. (In Dialog, it is hard-coded to find “Test Drive” and in FormFlow it is directly asking the detail).
To overcome this, Microsoft has come up with a really cool Language Understanding Intelligent Service, a.k.a LUIS. In this post, I’m not going to describe in detail on how to setup LUIS model (will do next time), but I would like to introducing its capability that is able to predict/interpret the intent of the user. For this sample, I’ve prepared the following LUIS model:
This model is configured to be able to interpret the intent of the user (Greeting, Test Drive, Ending Conversation, Brochure Request, None).
Now, below is the source code on how the Luis Dialog is built.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using CrmChatBot.CRM; | |
using CrmChatBot.Model; | |
using Microsoft.Bot.Builder.Dialogs; | |
using Microsoft.Bot.Builder.Luis; | |
using Microsoft.Bot.Builder.Luis.Models; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using System.Web; | |
namespace CrmChatBot.LUIS | |
{ | |
[LuisModel("LuisAppId", "LUIS Subscription Key")] | |
[Serializable] | |
public class CarInquiryLuisDialog : LuisDialog<object> | |
{ | |
protected TestDriveDetail testDriveDetail; | |
public const string Entity_Car_Make = "CarMake"; | |
public const string Entity_Car_Model = "CarModel"; | |
public const string Entity_Date = "builtin.datetime.date"; | |
private EntityRecommendation carMake; | |
private EntityRecommendation carModel; | |
private EntityRecommendation preferredDate; | |
[LuisIntent("")] | |
public async Task None(IDialogContext context, LuisResult result) | |
{ | |
string message = $"Sorry I did not understand: " + string.Join(", ", result.Intents.Select(i => i.Intent)); | |
await context.PostAsync(message); | |
context.Wait(MessageReceived); | |
} | |
[LuisIntent("Greeting")] | |
public async Task Greeting(IDialogContext context, LuisResult result) | |
{ | |
string message = $"Hi, is there anything that I could help you today?"; | |
await context.PostAsync(message); | |
context.Wait(MessageReceived); | |
} | |
[LuisIntent("Ending Conversation")] | |
public async Task Ending(IDialogContext context, LuisResult result) | |
{ | |
string message = $"Thanks for using Car Inquiry Bot. Hope you have a great day!"; | |
await context.PostAsync(message); | |
context.Wait(MessageReceived); | |
} | |
[LuisIntent("Test Drive")] | |
public async Task TestDrive(IDialogContext context, LuisResult result) | |
{ | |
testDriveDetail = new TestDriveDetail(); | |
PromptDialog.Text( | |
context: context, | |
resume: CarMakeHandler, | |
prompt: "What car make do you want to test?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
[LuisIntent("Brochure Request")] | |
public async Task BrocureRequest(IDialogContext context, LuisResult result) | |
{ | |
testDriveDetail = new TestDriveDetail(); | |
PromptDialog.Text( | |
context: context, | |
resume: BrochureRequestHandler, | |
prompt: "Which car do you want to get the brochure information?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
#region Brochure Request Handler | |
public virtual async Task BrochureRequestHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var car = await argument; | |
await context.PostAsync($"We have received your brochure request for {car}. Our sales team will send it out to you."); | |
PromptDialog.Confirm( | |
context: context, | |
resume: AnythingElseHandler, | |
prompt: "Is there anything else that I could help?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
#endregion | |
#region Test Drive Prompt | |
public virtual async Task CarMakeHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var carMake = await argument; | |
testDriveDetail.CarMake = carMake; | |
PromptDialog.Text( | |
context: context, | |
resume: CarModelHandler, | |
prompt: "What car model do you want to test?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task CarModelHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var carModel = await argument; | |
testDriveDetail.CarModel = carModel; | |
PromptDialog.Text( | |
context: context, | |
resume: PreferredTimeHandler, | |
prompt: "When would you like to come for test drive?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task PreferredTimeHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var prefTime = await argument; | |
testDriveDetail.RequestedTime = prefTime; | |
PromptDialog.Text( | |
context: context, | |
resume: CustomerNameHandler, | |
prompt: "Your name please?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task CustomerNameHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var customerName = await argument; | |
testDriveDetail.CustomerName = customerName; | |
PromptDialog.Text( | |
context: context, | |
resume: ContactNumberHandler, | |
prompt: "What is the best number to contact you?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
public async Task ContactNumberHandler(IDialogContext context, IAwaitable<string> argument) | |
{ | |
var contactNumber = await argument; | |
testDriveDetail.PhoneNumber = contactNumber; | |
await context.PostAsync($@"Thank you for your interest, your request has been logged. Our sales team will get back to you shortly. | |
{Environment.NewLine}Your test drive request summary: | |
{Environment.NewLine}Car Make: {testDriveDetail.CarMake}, | |
{Environment.NewLine}Car Model: {testDriveDetail.CarModel}, | |
{Environment.NewLine}Requested Time: {testDriveDetail.RequestedTime}, | |
{Environment.NewLine}Customer Name: {testDriveDetail.CustomerName}, | |
{Environment.NewLine}Phone Number: {testDriveDetail.PhoneNumber}"); | |
//CrmLead.CreateTestDrive(testDriveDetail, CrmDataConnection.GetAPI()); | |
CrmLead.CreateTestDrive(testDriveDetail, CrmDataConnection.GetOrgService()); | |
PromptDialog.Confirm( | |
context: context, | |
resume: AnythingElseHandler, | |
prompt: "Is there anything else that I could help?", | |
retry: "Sorry, I don't understand that." | |
); | |
} | |
#endregion | |
public async Task AnythingElseHandler(IDialogContext context, IAwaitable<bool> argument) | |
{ | |
var answer = await argument; | |
if (answer) | |
{ | |
await GeneralGreeting(context, null); | |
} | |
else | |
{ | |
string message = $"Thanks for using Car Inquiry Bot. Hope you have a great day!"; | |
await context.PostAsync(message); | |
context.Done<string>("conversation ended."); | |
} | |
} | |
public virtual async Task GeneralGreeting(IDialogContext context, IAwaitable<string> argument) | |
{ | |
string message = $"Great! What else that can I help you?"; | |
await context.PostAsync(message); | |
context.Wait(MessageReceived); | |
} | |
} | |
} |
That’s all for the sample codes! I hope this helps. For the code repository, feel free to have a look at github repo: https://github.com/andz88/CrmChatBot.
Stay tuned for the next part of this series: Deployment.
[…] the previous part of this series, we have seen the sample implementation and how the code is running against a CRM […]
[…] the previous part of this series, we have seen the sample implementation and how the code is running against a CRM […]
Thanks for sharing such a great article! I have a question about how I can get the ClientID?
hi, thanks for the good feedback. Which ClientID that you are referring to? Bot Framework App ID? or The Azure AD Client ID? In my sample, I’m not using the Web API that requires Azure AD Client ID. It was there for experimental.
Thanks for replying! I was referring to Client ID from Azure portal but I managed to get it now. I want to explore a little bit further to Cases in Dynamics Service, would you be able to give me some idea of how I can adjust the code?
Just change the function and data model at the end of the conversation to create Case entity instead of Lead.
This line in particular: CrmLead.CreateTestDrive(testDriveDetail, CrmDataConnection.GetOrgService());
Would you be able to guide me on how to create a case? I have tried searching over the technical document but still haven’t figured out yet.
I have another github repo for a CRM UG, that one creates Case: https://github.com/andz88/MelD365UGBot
Hi, would it be possible for you to provide the JSON for the LUIS?
Here you go: https://1drv.ms/u/s!AmGPnMIXbfFM02AekoGnlALClT7b
Hi Margono,
Nice job, congratulations.I take error which is “Exception: Object reference not set to an instance of an object.
[File of type ‘text/plain’]”.Is there any suggestion about this error?
Object reference not set = null exception. so, check whether the expected thing has value or not