• Kyle Hill

Kylie Bot Part 15 - Using an Azure Index with a bot

It is now time for us to update our Kylie Bot to look at the Azure Search index, instead of directly at the Dynamics 365 Customer Engagement database. First thing’s first, there is a trove of useful content here from Microsoft. In particular, we are going to use their pre-built wrappers, but more on that later.

To get started, we are going to add three projects to our solution. These projects will represent the Azure Search capabilities that I mentioned above. To keep the projects grouped, I am going to add a folder to our solution by right clicking on it and selecting ‘Add -> New Solution Folder’ like this:

Next, name the folder ‘Azure Search’ and add the three projects to it. Copies of the projects are available here:

Your solution should now look like this:

Next, in our RootDialog.cs class, we are going to need to add the Azure Search service we created previously here. To do this, we need to add the following instantiation to the class:

private ISearchClient searchClient;

We are also going to add the following constructor to the class to initialise the searchClient object:

public RootDialog(ISearchClient searchClient)

{

SetField.NotNull(out this.searchClient, nameof(searchClient), searchClient);

}

Once this is done, we are going to need to add a new Dialog for our searches. To do this, right click on the Dialogs folder in the KylieBot solution and select ‘Add -> Class’ like this:

Call the new class IndexSearchDialog.cs and you project should then look like this:

Add the following to your new IndexSearchDialog.cs class:

[Serializable]

public class IndexSearchDialog : SearchDialog

{

private static readonly string[] TopRefiners = { "Rating", "Product", "Version", "Category" , "Source" }

public IndexSearchDialog(ISearchClient searchClient) : base(searchClient, multipleSelection: true)

{

}

protected override string[] GetTopRefiners()

{

return TopRefiners;

}

}

Once this is done, we will also need to update our Web.config with the items that point to our actual Azure Search service by adding the following lines to the <appSettings> node:

<add key="SearchDialogsServiceName" value="SEARCH SERVICE NAME" />

<add key="SearchDialogsServiceKey" value="YOUR KEY" />

<add key="SearchDialogsIndexName" value="INDEX NAME" />

Next, we need to update the SearchHit.cs class in the recently added Search.Contracts Models folder. We need to update the properties to look as follows:

public string Id { get; set; }

public string Title { get; set; }

public string[] Keywords { get; set; }

public string Content { get; set; }

public string Rating { get; set; }

public int NumberOfRatings { get; set; }

public int TotalRatingScore { get; set; }

public string[] Tags { get; set; }

public string Product { get; set; }

public string Version { get; set; }

public string Category { get; set; }

public string Source { get; set; }

public string SourceLink { get; set; }

public DateTime LoadDate { get; set; }

public DateTime ArticleDate { get; set; }

public int MinorVersionNumber { get; set; }

public int MajorVersionNumber { get; set; }

Also, we will need to update the AddSelectedItem method in the SearchDialog.cs class in the Search.Dialogs project. Here we are replacing the term ‘Key’ with ‘Id’. Once that is done, we will need to update the SearchHitStyler.cs class as follows:

[Serializable]

public class SearchHitStyler : PromptStyler

{

public override void Apply<T>(ref IMessageActivity message, string prompt, IReadOnlyList<T> options, IReadOnlyList<string> descriptions = null, string speak = null)

{

var hits = options as IList<SearchHit>;

if (hits != null)

{

var cards = hits.Select(h => new ThumbnailCard

{

Title = h.Title,

Images = new[] { new CardImage("https://static.wixstatic.com/media/85fb2e_f0bfb249c6044df189ed617533f21a84~mv2.png/v1/fill/w_120,h_189,al_c/85fb2e_f0bfb249c6044df189ed617533f21a84~mv2.png") },

Buttons = new[] { new CardAction(ActionTypes.OpenUrl, "Pick this one", value: h.SourceLink) },

Text =

"**Product**: " + h.Product + "\n\n" +

"**Version**: " + h.Version + "\n\n" +

"**ArticleDate**: " + h.ArticleDate + "\n\n" +

"**Category**: " + h.Category + "\n\n" +

"**Rating**: " + h.Rating.ToString() + "\n\n" +

"**Source**: " + h.Source + "\n\n"

});

message.AttachmentLayout = AttachmentLayoutTypes.Carousel;

message.Attachments = cards.Select(c => c.ToAttachment()).ToList();

message.Text = prompt;

message.Speak = speak;

}

else

{

base.Apply<T>(ref message, prompt, options, descriptions, speak);

}

}

}

}

Finally, we will need to add a mapping class to our KylieBot project. To do this, right click on the Helpers folder in the project and select ‘Add -> Class’. Name your class: IndexMapper.cs and copy the following code into it:

public class IndexMapper : IMapper<DocumentSearchResult, GenericSearchResult>

{

public GenericSearchResult Map(DocumentSearchResult documentSearchResult)

{

var searchResult = new GenericSearchResult();

searchResult.Results = documentSearchResult.Results.Select(r => ToSearchHit(r)).ToList();

searchResult.Facets = documentSearchResult.Facets?.ToDictionary(kv => kv.Key, kv => kv.Value.Select(f => ToFacet(f)));

return searchResult;

}

private static GenericFacet ToFacet(FacetResult facetResult)

{

return new GenericFacet

{

Value = facetResult.Value,

Count = facetResult.Count.Value

};

}

private static SearchHit ToSearchHit(SearchResult hit)

{

return new SearchHit

{

Id = (string)hit.Document["id"],

Title = (string)hit.Document["Title"],

Keywords = (string[])hit.Document["Keywords"],

Content = (string)hit.Document["Content"],

Rating = hit.Document["Rating"] == null ? "" : (string)hit.Document["Rating"],

NumberOfRatings = hit.Document["NumberOfRatings"] == null ? 0 : (int)hit.Document["NumberOfRatings"],

TotalRatingScore = hit.Document["TotalRatingScore"] == null ? 0 : (int)hit.Document["TotalRatingScore"],

Tags = (string[])hit.Document["Tags"],

Product = (string)hit.Document["Product"],

Version = (string)hit.Document["Version"],

Category = (string)hit.Document["Category"],

Source = (string)hit.Document["Source"],

SourceLink = (string)hit.Document["SourceLink"],

LoadDate = ((DateTimeOffset)hit.Document["LoadDate"]).DateTime,

ArticleDate = ((DateTimeOffset)hit.Document["ArticleDate"]).DateTime,

MinorVersionNumber = int.Parse(hit.Document["MinorVersionNumber"].ToString()),

MajorVersionNumber = int.Parse(hit.Document["MajorVersionNumber"].ToString())

};

}

}

We will also need to add a reference to: Microsoft.Azure.Search. To do this, right click on the KylieBot project and select ‘Manage Nuget Packages…’ and you should see this:

Click on the ‘Browse’ tab in the top left hand corner and then type in Microsoft.Azure.Search and you should see this:

Select the Microsoft.Azure.Search option and the you should see the following:

Click on the grey ‘Install’ button to add the package.

We will also need to update our Global.asax.cs class with the following to include our Azure Search Client:

public class WebApiApplication : System.Web.HttpApplication

{

protected void Application_Start()

{

Models.AuthSettings.Mode = ConfigurationManager.AppSettings["ActiveDirectory.Mode"];

Models.AuthSettings.EndpointUrl = ConfigurationManager.AppSettings["ActiveDirectory.EndpointUrl"];

Models.AuthSettings.Tenant = ConfigurationManager.AppSettings["ActiveDirectory.Tenant"];

Models.AuthSettings.RedirectUrl = ConfigurationManager.AppSettings["ActiveDirectory.RedirectUrl"];

Models.AuthSettings.ClientId = ConfigurationManager.AppSettings["ActiveDirectory.ClientId"];

Models.AuthSettings.ClientSecret = ConfigurationManager.AppSettings["ActiveDirectory.ClientSecret"];

Models.AuthSettings.Scopes = ConfigurationManager.AppSettings["ActiveDirectory.Scopes"].Split(',');

Conversation.UpdateContainer(builder =>

{

builder.RegisterType<RootDialog>()

.As<IDialog<object>>()

.InstancePerDependency();

builder.RegisterType<IndexMapper>()

.Keyed<IMapper<DocumentSearchResult, GenericSearchResult>>(FiberModule.Key_DoNotSerialize)

.AsImplementedInterfaces()

.SingleInstance();

builder.RegisterType<AzureSearchClient>()

.Keyed<ISearchClient>(FiberModule.Key_DoNotSerialize)

.AsImplementedInterfaces()

.SingleInstance();

});

GlobalConfiguration.Configure(WebApiConfig.Register);

}

}

And the following in our MessageController.cs class to leverage the above:

using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))

{

await Conversation.SendAsync(activity, () => scope.Resolve<IDialog<object>>());

}

We also need to modify some of the default flow in the IndexSearchDialog.cs class in the KylieBot project. We are going to disable the functionality of multiselect and adding search results to a list as this will not be relevant initially. This is done by setting the multiselect option to false like this:

public IndexSearchDialog(ISearchClient searchClient) : base(searchClient, multipleSelection: false)

We also need to add a ProcessActionDialog.cs class to handle all of the options we can serve to the user like this:

[Serializable]

public class ProcessActionDialog : IDialog<object>

{

private ISearchClient searchClient;

public ProcessActionDialog(ISearchClient searchClient)

{

this.searchClient = searchClient;

}

public Task StartAsync(IDialogContext context)

{

var userData = context.UserData;

User retrieveUser = userData.GetValue<User>("User");

switch (retrieveUser.searchTerm)

{

case "Empowered Search":

context.Call(new IndexSearchDialog(this.searchClient), SearchCompleted);

break;

case "Back To Kylie Bot":

context.Done<string>(null);

break;

case "Additional Info":

break;

case "I'm done for now":

context.PostAsync("Hope to see you again soon. Just send me a message if you need to wake me up.");

context.EndConversation("");

context.Done<string>(null);

break;

default:

context.Done<string>(null);

break;

}

return Task.CompletedTask;

}

private Task SearchCompleted(IDialogContext context, IAwaitable<object> result)

{

context.Done<string>(null);

return Task.CompletedTask;

}

public Task SearchCompleted(IDialogContext context, IAwaitable<IList<SearchHit>> result)

{

context.Done<string>(null);

return Task.CompletedTask;

}

}

I also updated the conversation in the RootDialog.cs and MessageController.cs classes, so look out for that. While debugging, there was a very helpful article available from Microsoft here. In particular, the section on ‘Look for exceptions’ which ensures your debugging settings are correct in Visual Studio.

Finally, we are going to need to include the search capabilities are part of our existing conversation flow. To do this, update the RootDialog.cs class as follows:

context.Call(new IndexSearchDialog(this.searchClient, retrieveUser.searchTerm), SearchCompleted);

Once this is done, you can go ahead an right click on the KylieBot project and select ‘Publish…’ like this:

The publishing profile should already be stored from our previous deployments and you should be able to click on the ‘Publish’ button like this:

Once the deployment has finished, go and check out the results on your portal where the bot is deployed and you should observe something similar to this:

Now that we’ve completed that, don’t forget to check in your code and stay tuned for the next post on using sentiment with our bot conversations.

The completed project is available here.

#KylieBot #AzureIndex

As an innovative technology pioneer, I focus on the Microsoft Business Applications platform and on creating value-generating solutions, which also include IoT, AI and Mixed Reality.

See this blog and more on the Dynamics Community and my MVP profile

  • RSS Social Icon
  • Facebook Social Icon
  • LinkedIn Social Icon
  • Twitter Social Icon
  • Instagram Social Icon

© 2020 by Kyle Hill

London, UK

kyle@daringdynamics.co.uk