Personality Chat Integration to Bots

There is a library and service called Personality Chat that can be integrated to a chat bot so add small talk capabilities. The interesting part of it is that you can provide a personality type like professional, friendly, humorous.

I have a LUIS chat bot, so it is an exciting option to me to integrate this capability to my bot. I have been using BotBuilder v4 SDK, and Personality Chat provides an integration point with that too. Integration itself seems simple, based on the provided samples and documentation, all you need is to setup a middleware for your bot during startup.

Tryout

The real story is a bit more involved, hence this post is born. The current status of the library is an alpha nuget package. It is referencing an alpha build of the BotBuilder v4 SDK too, which contains breaking changes. For this reason, you won't be able to add middleware to your bot, you will get a compile error.

An alternative option is to create your own IMiddleware implementation, and use that instead of the one provided within the package. I tried this solution myself too, but soon I realized this is not a good route to follow.

Firstly, the service behind Personality Chat is throttled to 30 queries per minute, and I did not see an option to buy in for more quotas.

Secondly, the primary way my bot is used is not chit-chat. It is used with a reason, adding chitchat is just a nice touch. For this reason, having every chat message tested against the personality service is not a good option. Most of the messages won't be chitchat, so it is just a waste of time and resources.

Thirdly, it is slow. According to my initial testing, it took about a second to get a response if the chat message has an adequate answer by the service, or I shall handle it by my bot. This hit is fine for a single message that I would not be able handle myself in the bot, but not for every message as part of a middleware.

A different route

For the above reasons I came up with a different solution. I added a new intent to my LUIS bot, called it 'chitchat'. I opened up the data source of Personality Chat (it is available on GitHub), and added a couple of hundred sample questions to the intent's sample. This way when some non-business chitchat message arrives, it would end up categorized to this intent.

Next, I modified my application. Added a handler to my new intent.

_dialogs.Add(new WaterfallDialog("chitchat", new[] { new WaterfallStep(GetChitChat) }));

All it does is invoking the service itself:

private async Task<DialogTurnResult> GetChitChat(WaterfallStepContext step, CancellationToken token)
{
  if (await _personalityChat.HandleChatAsync(step.Context, token))
    return await step.EndDialogAsync();
  await step.Context.SendActivityAsync("I am not sure if I can help with that.", "I am not sure if I can help with that.");
  return await step.EndDialogAsync();
}

To make it work I have a _personalityChat injected, which is a new type PersonalityChatInvoker, will be shown later. It has some extensions and registration during startup:

services.AddPersonalityChitChat(new PersonalityChatMiddlewareOptions(
botPersona: Microsoft.Bot.Builder.PersonalityChat.Core.PersonalityChatPersona.Professional, respondOnlyIfChat: true, scoreThreshold: 0.3F));

The AddPersonalityChitChat is an extension method:

public static IServiceCollection AddPersonalityChitChat(this IServiceCollection services, PersonalityChatMiddlewareOptions options)

  services.AddHttpClient();
  services.AddSingleton(options);
  services.AddSingleton<PersonalityChatInvoker>();
  return services;
}

It adds a HttpClient dependencies because the library internally uses HttpClient is a less efficient way. Adds the options and new class PersonalityChatInvoker. Let's look at it:

public class PersonalityChatInvoker
{
  private readonly PersonalityChatService _service;
  private readonly PersonalityChatMiddlewareOptions _personalityChatMiddlewareOptions;

  public PersonalityChatInvoker(PersonalityChatMiddlewareOptions personalityChatMiddlewareOptions, IHttpClientFactory clientFactory)
  {
      //Initializing fields not included.
  }

  public async Task<bool> HandleChatAsync(ITurnContext context, CancellationToken cancellationToken = default)
  {
    IMessageActivity activity = context.Activity.AsMessageActivity();
    if (activity != null && !string.IsNullOrEmpty(activity.Text))
    {
      PersonalityChatResults personalityChatResults = await _service.QueryServiceAsync(activity.Text.Trim()).ConfigureAwait(false);
      if (!_personalityChatMiddlewareOptions.RespondOnlyIfChat || personalityChatResults.IsChatQuery)
      {
        return await PostPersonalityChatResponseToUser(context, GetResponse(personalityChatResults));
      }
    }
    return false;
  }

  private string GetResponse(PersonalityChatResults personalityChatResults)
  {
    List<PersonalityChatResults.Scenario> list = personalityChatResults?.ScenarioList;
    string result = string.Empty;
    if (list != null)
    {
      PersonalityChatResults.Scenario scenario = list.FirstOrDefault();
      if (scenario?.Responses != null && scenario.Score > _personalityChatMiddlewareOptions.ScoreThreshold && scenario.Responses.Count > 0)
      {
        int index = new Random().Next(scenario.Responses.Count);
        result = scenario.Responses[index];
      }
    }
    return result;
  }

  private async Task<bool> PostPersonalityChatResponseToUser(ITurnContext turnContext, string personalityChatResponse)
  {
    if (!string.IsNullOrEmpty(personalityChatResponse))
    {
      await turnContext
        .SendActivityAsync(personalityChatResponse, personalityChatResponse, InputHints.AcceptingInput)
        .ConfigureAwait(false);
      return true;
    }
    return false;
  }
}

In short it has similar responsibility as the middleware had: it invokes a service through PersonalityChatService, checks if the response's score meets with the options, and returns the response message to the user if it is a hit for chitchat message. Note, that I am reusing the build in personas and MiddlewareOptions, from the original library.

Finally, let's look at the PersonalityChatService:

public PersonalityChatService(PersonalityChatOptions personalityChatOptions, IHttpClientFactory clientFactory)
{
  _personalityChatOptions = personalityChatOptions ?? throw new ArgumentNullException(nameof(personalityChatOptions));
  _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
}

public async Task<PersonalityChatResults> QueryServiceAsync(string query)
{
    HttpClient client = _clientFactory.CreateClient();
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", _personalityChatOptions.SubscriptionKey);
    client.Timeout = TimeSpan.FromMilliseconds(5000.0);
    StringContent content = new StringContent(JsonConvert.SerializeObject(new PersonalityChatRequest(query, _personalityChatOptions.BotPersona)), Encoding.UTF8, "application/json");
    HttpResponseMessage response;
    using (response = await client.PostAsync(_uri, content))
    {
      response.EnsureSuccessStatusCode();
      var contentStream = await response.Content.ReadAsStreamAsync();
      using (var reader = new StreamReader(contentStream))
      {
        using (var textReader = new JsonTextReader(reader))
        {
          var serializer = new JsonSerializer();
          return serializer.Deserialize<PersonalityChatResults>(textReader);
        }
      }
    }
  }
}

The reason for this class is just to ensure that use HttpClient in a more correct way. Note, this is not production ready code yet, it is more of a suggested approach.

Summary

With this approach I can eliminate the disadvantages of the middleware, and still use personality as part of my LUIS bot.