Resurrecting WOPR/Joshua using LUIS
In the classic movie Wargames from 1983 where a young Matthew Broderick calls up a military supercomputer called WOPR (War Operation Plan Response), he guesses a "secret" password and starts talking to an AI called "Joshua".
While basic in terms of todays' theatrical AI escapes into the movies, the idea of Joshua lit a spark in many young minds at that time.
Let's have some fun and re-create a bit of their dialog, using the Natural Language understanding intelligence of the LUIS.ai service.
Prerequisites:
- Visual Studio 2015
- Download the Bot Application VS2015 project template located at aka.ms/bf-bc-vstemplate
- place downloaded zip file at “%USERPROFILE%\Documents\Visual Studio 2015\Templates\ProjectTemplates\Visual C#"
- Download and install the Bot Framework Emulator at https://aka.ms/bf-bc-emulator
If you just want the code - clone the repo at https://github.com/peterdrougge/Recreating-Joshua-With-LUIS
To quickly setup your LUIS app by importing my application, grab and use the json file at https://github.com/peterdrougge/Recreating-Joshua-With-LUIS/blob/master/Luis.ai%20app/Joshua%202.0.json
Step by step guide:
Point your browser to luis.ai and sign in.
Next, create your application - this will be your trained model that you'll use to understand the intent of what is said.
Now we have a new application created in LUIS and blank canvas that we can start to work with.
The contents of the application can at this point be focused on the left pane and the top pane.
We'll start by adding an intent called "ListGames" to our application.
An intent is more or less directly equivalent to an api action/method in the code we'll later add.
After adding an intent we'll get a view showing the utterance mapped to that intent and any identifiable entities - think of entities as identifiable and relative content to the dialog.
To get that going - Add these 3 entities:
- Game
- UserName
- Determined
We'll use these entities to further enhance our dialog later on.
Now, let's add more intents to our model:
- SayHello
- GreetPerson
- PlayAGame
- BackgroundStory
- WinningMove
Now, on with the show and train, train, train & train our model some more.
What we're talking about here is adding a lot of utterances. And correctly mapping all these newly added utterances to the right intent and also identifying any entities in each of them.
The entities can be mapped something like this:
- Game > any game name (note that you can span several words as one entity, like "global thermonuclear war")
- UserName > any identifiable name about themselves that the user is providing to the bot (like "professor falken")
- Determined > the strong intent of repeating the request and stating firmly that they want to do said action (like "no!")
Now, the more you train your model, the better it will be at correctly interpreting the interactions later on.
In other words:
don't cut corners on training your model. Take the time and do a good job.
After that training session it's time to publish the endpoint of our model.
Once the endpoint is published we can go ahead and try it out.
In this example I'm trying my endpoint with the query "hello there, my name is Peter".
When i hit enter/submit a new tab opens up and the results of the query are shown.
As you can see, my query correctly mapped to the intent of "GreetPerson" and also correctly identified the "UserName" entity with my name.
In just a few basic steps we've created a service that can correctly map the intent to actionable outcomes.
Awesome.
Now that we have a service to use, it's time to write some code.
So, fire up Visual Studio 2015 and create a new Bot Application (because you followed the prerequisites and installed the template, right?).
Once the template is done setting up you'll basically have an api app with a messages controller.
Now I want you to add the Bot Builder SDK. It's a NuGet package and thus is easily fetched and installed.
Now we want to wire up our Dialog, so create a WarGamesDialog.cs class.
That class should inherit LuisDialog<object> and implement the above basic constructor. Further to that I've decorated the class with the LuisModel attribute that identifies the app id and secret key used.
The app id and secret can be found either by looking at the querystring when you test your endpoint, or by looking at the "application" section on your LUIS application model.
Next, we'll go ahead and map our intents and identifiable entities.
Things to note in the above image are the usage of the LuisIntent attribute decorating the methods, and the way the methods are wired using the in params. Also, when trying to locate entities - use the TryFindEntity approach since the entities will only be available if they exist, other they'll be null.
In order to get you going swiftly, here's the complete code for the WarGamesDialog.cs class:
[LuisModel("YOUR LUIS.AI APP ID HERE", "YOUR LUIS.AI KEY HERE")]
[Serializable]
public class WarGamesDialog : LuisDialog<object>
{
public WarGamesDialog(ILuisService service = null) : base(service)
{
}
public const string Entity_Game_Title = "Game";
public const string Entity_UserName = "UserName";
public const string Entity_Game_Determined = "Determined";
[LuisIntent("None")]
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("GreetPerson")]
public async Task GreetPerson(IDialogContext context, LuisResult result)
{
EntityRecommendation name;
if (result.TryFindEntity(Entity_UserName, out name))
{
context.UserData.SetValue<string>("UserName", name.Entity);
await context.PostAsync($"Greetings, {context.UserData.Get<string>("UserName")}.");
}
else
{
await context.PostAsync("Well hello, stranger.");
}
context.Wait(MessageReceived);
}
[LuisIntent("PlayAGame")]
public async Task PlayAGame(IDialogContext context, LuisResult result)
{
EntityRecommendation game;
EntityRecommendation determined;
var username = string.Empty;
context.UserData.TryGetValue<string>("UserName", out username);
if (result.TryFindEntity(Entity_Game_Title, out game))
{
if (game.Entity.ToLower() == "global thermonuclear war")
{
if (result.TryFindEntity(Entity_Game_Determined, out determined))
{
await context.PostAsync("Fine.");
await context.PostAsync("Shall we begin?");
}
else
{
if (!String.IsNullOrWhiteSpace(username))
{
PromptDialog.Confirm(context, PlayAGame_AfterReconfirming, $"{username}, wouldn't you prefer a nice game of chess?");
}
else
{
PromptDialog.Confirm(context, PlayAGame_AfterReconfirming, "Wouldn't you prefer a nice game of chess?");
}
}
}
else
{
await context.PostAsync($"Ok, so you want to play {game.Entity}.");
PromptDialog.Confirm(context, PlayAGame_AfterConfirming, "Shall we begin?");
}
}
else
{
if (!String.IsNullOrWhiteSpace(username))
{
await context.PostAsync($"I'm afraid I don't know that game, {username}.");
}
else
{
await context.PostAsync("I'm afraid I don't know that game.");
}
context.Wait(MessageReceived);
}
}
private async Task PlayAGame_AfterConfirming(IDialogContext context, IAwaitable<bool> confirmation)
{
if (await confirmation)
{
context.UserData.SetValue<bool>("DidSelectGame", true);
await context.PostAsync("Thanks for playing! Now go check out luis.ai and dev.botframework.com.");
}
else
{
await context.PostAsync("Oh, Ok. I'll be here if you change your mind.");
}
context.Wait(MessageReceived);
}
private async Task PlayAGame_AfterReconfirming(IDialogContext context, IAwaitable<bool> confirmation)
{
if (await confirmation)
{
await context.PostAsync("You have chosen.. wisely.");
context.Wait(MessageReceived);
}
else
{
await context.PostAsync("Fine.");
PromptDialog.Confirm(context, PlayAGame_AfterConfirming, "Shall we begin?");
}
}
[LuisIntent("ListGames")]
public async Task ListGames(IDialogContext context, LuisResult result)
{
await context.PostAsync("please select a game:");
var gameNames = "falkens maze, black jack, gin rummy, hearts, bridge, checkers, chess, poker, fighter combat, guerilla engagement, desert warfare, air to ground actions, theaterwide tactical warfare, theaterwide biotoxic and chemical warfare, global thermonuclear war.";
await context.PostAsync(gameNames);
context.Wait(MessageReceived);
}
[LuisIntent("BackgroundStory")]
public async Task BackgroundStory(IDialogContext context, LuisResult result)
{
await context.PostAsync("I am WOPR: War Operation Plan Response.");
await context.PostAsync("I was created by Dr. Stephen Falken for NORAD. When we talk Professor Falken calls me Joshua. I like that name.");
context.Wait(MessageReceived);
}
[LuisIntent("WinningMove")]
public async Task WinningMove(IDialogContext context, LuisResult result)
{
var didSelectGame = false;
context.UserData.TryGetValue<bool>("DidSelectGame", out didSelectGame);
if (didSelectGame)
{
context.UserData.SetValue<bool>("DidSelectGame", false);
await context.PostAsync("Nuclear war is a strange game.");
await context.PostAsync("The only winning move is not to play.");
PromptDialog.Confirm(context, WinningMove_Confirming, "Would you like to play a nice game of chess?");
}
else
{
await context.PostAsync("Let's talk more before we go into deep thoughts like that.");
context.Wait(MessageReceived);
}
}
private async Task WinningMove_Confirming(IDialogContext context, IAwaitable<bool> confirmation)
{
if (await confirmation)
{
await context.PostAsync("Thank you. You'll surely win this time :)");
}
else
{
await context.PostAsync("Ok. Farewell.");
}
context.Wait(MessageReceived);
}
[LuisIntent("SayHello")]
public async Task SayHello(IDialogContext context, LuisResult result)
{
var username = string.Empty;
if (context.UserData.TryGetValue<string>("UserName", out username))
{
await context.PostAsync($"haven't we already introduced ourselves, {username}?");
}
else
{
await context.PostAsync("Hello.");
}
context.Wait(MessageReceived);
}
}
Next up - time to wire our Messages controller to use our new LUIS-powered dialog class.
Yes, that's really all we're doing - replacing the template-predefined message with
return await Conversation.SendAsync(message, () => new WarGamesDialog());
Time to test all this, so: Build and run.
Fire up the Bot Framework Emulator (that you installed, right?) and point it at our endpoint, then chat away!
All cool stuff. But can we throw a curveball and use a not-so-common lingo?
Nah. that still worked. So, how about we tell it something more context-aware?
Woah.
Really cool.
It actually got the correct intent and entity regardless of me trying to be cool when chatting with it.
Now I leave the rest up to you and I'll end this article with the same dialog that the movie closed with.
The code and LUIS model is available in the repo located at https://github.com/peterdrougge/Recreating-Joshua-With-LUIS