Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / Azure

Azure Animal Adoption Agent and Lost & Found

5.00/5 (32 votes)
7 Oct 2020CPOL68 min read 121.9K   687  
Azure based pet adoption agent that helps pet lovers find the perfect pet while saving the lives of kittens & puppies
In this article, you will learn how to do convenient two-way data binding to an enumerated property in a Windows Phone app!

NOTE: Learn how to do convenient two-way data binding to an enumerated property in a Windows Phone app! (See below after the system architecture diagram.)

(Windows Azure Contest Entry)

Table of Contents

Phase 1 - Project Overview

Why Azure?

Azure has both software, platform, and infrastructure as a service features, making it the ultimate solution for creating high performance, reliable, ubiquitous client/server solutions. The Azure platform eliminates the vast amount of time and cost normally involved in distributed database application development, allowing a developer like myself to concentrate my time on creating features & value for my users instead of wasting it on support code and infrastructure.

(Note: I already have an Azure account via MSDN.)

Introduction

Azure is the perfect platform for coordinating the search and notification services vital to the creation of a pet adoption agent that connects potential pet owners with the countless adorable kittens & puppies just waiting to give love. By providing cloud based, easy to configure SQL storage Azure is perfect for storing the search criteria specified by eager pet parents-to-be via the FindAPet client app running on their Windows Phone. Then Azure Mobile Services is leveraged to send out push notifications to them when pets that meet their stored preferences become available at animal shelters throughout the USA. The PetFinder API provides ready access to an inventory of adoptable pets of nearly every type and breed and direct access to the animal shelters that house them. It also provides the necessary animal and shelter search facilities necessary to complete the system, a system that will save the lives of countless innocent loving animals when it's finished.

An auxiliary feature of the system will be a Lost & Found service. Owners of lost pets can enter a description of the pet they lost. In a manner similar to Google Alerts, the system will run periodic sweeps of shelters within a 50 mile radius of the owner's home location. If a pet matching the owner's description is found, a push notification will be sent to the owner with a link to the shelter that is holding the pet. (Thank you Simon Jackson for the Lost & Found idea!)

Overview

The entry point into the system will be the FindAPet app (client) running on the Windows Phone platform using the MVVM Light framework. It will provide a simple search interface that helps them find the kind of pet they are interested in adopting. The screens below show a sample session where the user is looking to adopt a Calico kitten:

The system then shows them a list of animal shelters near them. Using the phone's Geolocation services, the user's current location is used as the center point for a proximity search, as shown in the screenshot below where Boise, Idaho was identified as the user's current location. The user checks off the shelters that they are willing to travel to:

Select Shelters

The user's pet and shelter preferences are stored in an Azure hosted SQL table. As pets at the designated shelters become available, a push notification is sent to the user with the prospective pet's details and photo for easy viewing. If they like the pet and are considering adopting it, they can add the pet to their wishlist, as shown in the screen shot below (note an adult cat is shown too since the user selected it in a search where the specified age was adult):

Push notifications will also act as reminders to notify the user when one of the candidate pets is close to their termination date, to make sure they don't miss the opportunity to save a life while finding a lifelong companion. Finally, the system will leverage the Nokia Here Maps service to give them directions to the shelter where the chosen pet is at, so they can pick up their new loved one and return home together to share many happy memories.

System Architecture


Two-Way Data Binding to an Enumerated Property Without Creating New Value Converters or Creating Auxiliary Properties (MVVM Light)!

Code Project is about source code and so is this contest so I am including the source code for a helpful utility class that I created while working on the FindAPet Windows Phone client. I became tired of creating auxiliary properties to provide types that were easy to data bind to, just to expose an enumerated property for binding. Just as tedious was creating a new value converter for each enumeration type. The attached source file gives you a class you can drop into your MVVM Light C# project. The code provides a generic value converter for enumerated types so you can create the binding completely through the use of the Create New Data Binding dialog options without having to add any support code to make it happen.

With this code, you now have a value converter for any ViewModel enumerated property that does two-way conversion between the Description attribute strings for the enumerated values and the enumeration constants they represent. This allows you to do two-way data binds to an enumerated property without having to do any plumbing code. The only thing you have to do is set the Converter field to EnumToDescAttrConverter and put the fully qualified type name for the Enumeration type of the bound property in the ConverterParameter field (see picture below for an example). If you have trouble determining the correct fully qualified Enum type name, just set a break point in the ConvertBack() method where an Exception will be thrown. Then call System.Reflection.Assembly.GetExecutingAssembly().DefinedTypes.ToList() in the Immediate Window to get a list of all currently defined System types in that execution context. Find the correct fully qualified type name and paste it into the ConverterParameter field.

Here's how it works. Whenever the view accesses the bound enumerated property, EnumToDescAttrConverter gets the description attribute for the enum when its Convert() method is called:

C#
// Consumer wants to convert an enum to a description 
//  attribute string.
public object Convert(
                 object value, 
                 Type targetType, 
                 object parameter, 
                 CultureInfo culture)
{
    // Since we don't know what the correct default 
    //  value should be, a NULL value is unacceptable.
    if (value == null)
        throw new ArgumentNullException(
            "(EnumToDescAttrConverter:Convert) The value is unassigned.");

    Enum e = (Enum)value;

    return e.GetDescription();
}

This gives list boxes and other view elements a nice human readable string to work with. Now what happens when the view wants to update an enumerated property with a new value? For example, the user makes a ListBox selection thus triggering a property set call because the ListBox's SelectedItem property is bound to the enumerated property. This is where the fully qualified type name entered as the ConverterParameter comes into play.

The EnumToDescAttrConverter.ConvertBack() method uses the fully qualified type name passed via the Parameter parameter to create the correctly typed concrete enum value using Reflection. It then searches the description attributes for that enum type to find the enum value associated with the description given in the value parameter, and returns that enum value.

C#
// Convert an enumeration value in Description attribute form 
// back to the appropriate enum value.
public object ConvertBack
       (object value, Type targetType, object parameter, CultureInfo culture)
{
    // Since we don't know what the correct default value should be, 
    // a NULL value is unacceptable.
    if (value == null)
        throw new ArgumentNullException(
        	"(EnumToDescAttrConverter:ConvertBack) The value is unassigned.");

    string strValue = (string)value;

    // Parameter parameter must be set since it must contain the concrete Enum class name.
    if (parameter == null)
        throw new ArgumentNullException(
        	"(EnumToDescAttrConverter:ConvertBack) The Parameter parameter is unassigned.");

    string theEnumClassName = parameter.ToString();

    // Create an instance of the concrete enumeration class from the given class name.
    Enum e = (Enum)System.Reflection.Assembly.GetExecutingAssembly().CreateInstance
             (theEnumClassName);

    if (e == null)
        throw new ArgumentException(
            "(EnumToDescAttrConverter:ConvertBack) Invalid enumeration class name: " 
            + theEnumClassName
            + ". Set a break point here and call "
            + "System.Reflection.Assembly.GetExecutingAssembly().DefinedTypes.ToList()"
            + " in the immediate window to find the right type.  Put that type into "
            + "the Converter parameter for the data bound element you are working with."
            );

    System.Type theEnumType = e.GetType();

    Enum eRet = null;

    // Now search for the enum value that is associated with the given description.
    foreach (MemberInfo memInfo in theEnumType.GetMembers())
    {
        object[] attrs = memInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);

        if (attrs != null && attrs.Length > 0)
        {
            if (((DescriptionAttribute)attrs[0]).Description == strValue)
            {
                // Ignore the case
                eRet = (Enum)Enum.Parse(theEnumType, memInfo.Name, true);
                break; // Found it.
            }
        }
    } // foreach (MemberInfo memInfo in typeof(TEnum).GetMembers())

    // If the string can not be converted to a valid enum value, throw an
    //  Exception.
    if (eRet == null)
        throw new ArgumentException(
            String.Format("{0} can not be converted to an enum value: ", strValue));

    return eRet;
}

Note: Use the ToDescriptionsList<>() method to conveniently grab the description attributes from an Enum type to fill a ListBox or other similar element. Put the call to it in the property that returns a list of human friendly strings to the UI element bound to the enumerated property. (For example, the ItemsSource property of a list box):

C#
public static List<string> ToDescriptionsList<t>()
{
    // GetValues() is not available on Windows Phone.
    // return Enum.GetValues(typeof(T)).Cast<t>();
    List<string> listRet = new List<string>();

    foreach (var x in typeof(T).GetFields())
    {
        Enum e;

        if (x.IsLiteral)
        {
            e = (Enum)x.GetValue(typeof(Enum));

            listRet.Add(e.GetDescription());
        } // if (x.IsLiteral)
    } // foreach()

    return listRet;
}

Below is the enumerated type that represents the different kinds of animals the PetFinder API can search for. This is an example of using the Description attribute to tie a human friendly string to an enum value:

C#
// Example of an Enumerated type with Description Attributes
   // (barnyard, bird, cat, dog, horse, pig, reptile, smallfurry)
   // List of Animal types the breed list method accepts.
   public enum EnumAnimalType
   {
       [Description("Barnyard")]
       barnyard,
       [Description("Birds")]
       bird,
       [Description("Cats & Kittens")]
       cat,
       [Description("Dogs & Puppies")]
       dog,
       [Description("Horses & Ponies")]
       horse,
       [Description("Pigs")]
       pig,
       [Description("Reptiles")]
       reptile,
       [Description("Other Small & Furry")]
       smallfurry
   }

Example of a two-way data binding using the generic converter class, with the fully qualified type name I discovered via the Immediate Windows:


Phase 2 - Build and Deploy an Azure Web Site Fast with WebMatrix 3

Speed! I built and deployed this entire Lost & Found web site that can detect a cat's face in a photo in only one day! Even better, with WebMatrix 3's Azure support, you don't have to use TFS, GitHub, or any other repository to deploy your site. Just edit your site locally and when you are ready, hit the Publish key! You can even edit a remote site on Azure directly if you want!

Also, the web site described in this article owes much to the wonderful KittyDar project, which is the source of the cat face detection technology used by the web site to ensure the quality of uploaded Lost & Found photos. The web site has been live for a week now. You can experiment with the web site and the cat face detection technology using the link below. You do not need to register to use the web site, only to upload cat photos of your own:

FindAPet Lost & Found Web Site

One of Azure's biggest benefits is the ability to create and deploy professional web sites at breakneck speed. With WebMatrix 3's seamless integration with Azure, in most cases, all you have to do to publish your web site to Azure or update it is to press the Publish button in WebMatrix 3! In this section of my Code Project article for the Azure Developer Challenge, I will show you how I built a web site that accepts photos of a lost cat from worried pet owners, and then checks the quality of the photo to make sure it is suitable for use by others to identify their cat easily. You will find the WebMatrix 3 project replacement files attached as a download to this article. The instructions for using them follow below.

IMPORTANT: Remember to update the ReCAPTCHA keys in the source with the keys that belong to you, otherwise you will get strange errors on the account registration page! I have removed my keys and restored the placeholder strings PUBLIC_KEY and PRIVATE_KEY as per the Photo Gallery tutorial (covered later in the article). Follow that tutorial's instructions carefully regarding the ReCAPTCHA keys and you will not have any problems using CAPTCHA on your site.

Detecting Cat Faces and The Lost & Found Web Site

As mentioned in the project overview, the Azure Animal Adoption Agent will have a Lost & Found component that allows worried owners of missing pets to upload pictures of their loved ones, to help others find them. The site will also be available to pet shelters so they can scan pictures of pets whose owners live close to the shelter, to see if they have picked up a stray that belongs to one of those owners. The shelter employee can then contact the pet owner to give them the happy news.

Thanks to the KittyDar Cat Detector project, cat owners will have an advantage. Since KittyDar only detects cat faces if the cat is looking at the camera in a portrait orientation, it can be used to assist cat owners in submitting a photo that is as helpful as possible in identifying their pet. This stops the owner from uploading a photo that may have sentimental value but isn't very useful for identifying the cat. In addition, KittyDar can detect if there are multiple cat faces in the photo. In that case, the owner will be instructed to use a different photo that only has their cat in the shot.

Here are some example photos from the FindAPet web site that illustrate the benefits of the KittyDar cat face detector. The red bounding rectangle shown in photos where a cat's face was detected successfully shows the location of the cat's face as determined by the detection engine:

Photo 1 - Single cat face detected. Photo is suitable for Lost & Found purposes.

KittyDar Detection Failed

Photo 2 - Detection Failed. Cat's Face is not Oriented Properly. Unsuitable for Lost & Found Purposes

KittyDar Too Many Cats

Photo 3 - Detection Succeeded but Too Many Cats. Unsuitable for Lost & Found Purposes

WebMatrix 3 - Power Tool for Rapid Web Site Deployment with Azure

WebMatrix 3 is the latest release of Microsoft's tool for rapid web site development. The most powerful feature of this release is its close integration with Azure. This allows you to create and deploy web sites effortlessly, even sites backed by databases like the FindAPet Lost & Found Cat web site. With the right starting template, WebMatrix 3 will handle all the tedious tasks involved in creating and configuring your site on Azure.

As the basis for my Lost & Found web site, I used the WebMatrix 3 Photo Gallery template. The attached download gives you everything you need to recreate the Lost & Found Cat web site on your Azure account. But do read the sections below where I explain the various places I modified the code generated by the Photo Gallery Template. You will learn the important locations of the code where you may want to make changes to adapt the project for your own needs. You will also learn about a few fixes I needed to make to the default template code to make it work right. If these fixes are not part of the template code at the time you read this, you can use this knowledge to save time implementing your own project based on the Photo Gallery Template. If you just want to create your own KittyDar Lost & Found web site and are not interested in the details, then skip the Long Form Explanation section below and jump to the Short Form Explanation.

Here are the detailed steps I took to create the web site.


Long Form Explanation

Detailed Steps To Reproduce the Lost & Found Photo Gallery with Cat Face Detector

IMPORTANT: Remember to update the ReCAPTCHA keys in Register.cshtml located in the Account folder with the keys that belong to you! Replace the PUBLIC_KEY and PRIVATE_KEY string constants with your ReCAPTCHA keys as per the Photo Gallery tutorial. Follow that tutorial's instructions carefully on the keys and you will not have any problems using CAPTCHA on your site.

Step 1

Follow the instructions in this tutorial to get the WebMatrix 3 Photo Gallery template up and running:

WebMatrix 3 Photo Gallery Tutorial

At the time this was written, the error message for an unsuccessful CAPTCHA attempt displays before a CAPTCHA attempt is even made. To correct this, replace the line in Register.cshtml that says:

C#
@Html.ValidationSummary("Account creation was not successful. 
Please correct the errors and try again.", excludeFieldErrors: true, htmlAttributes: null)

with:

C#
@if(IsPost == true )
{
    @Html.ValidationSummary( "Account creation was not successful. 
    Please correct the errors and try again.", excludeFieldErrors: true, htmlAttributes: null)
}

Step 2

At the time this was written, the Photo Gallery template was referencing an old version of JQuery in the site layout file, _SiteLayout.cshtml. If this is still the case, open that file and replace the line SCRIPT element in the HEAD element that references version 1.8.2 with the following snippet:

HTML
<!-- The line below references an old version of jquery and was commented out -->
<!-- <script src="~/Scripts/jquery-1.8.2.min.js"></script>-->
<!-- The line references the newer version of jquery included in the Photo Gallery template -->
<script src="~/Scripts/jquery-2.0.0.min.js"></script>

Step 3

Publish the Photo Gallery web site to your Azure account and inspect it. You should now be ready for the real fun.

Step 4

Add the KittyDar JavaScript files to your WebMatrix 3 project Scripts folder. You can find these files in the Scripts folder found in the download attached to this article:

  • kittydar-0.1.0.min.js
  • kittydar-0.1.6.js
  • kittydar-demo.js
  • kittydar-detection-worker.js

These files provide the code necessary to detect cat faces in photos and to draw the bounding rectangles on the photo while detection is taking place and after a detection has succeeded.

Step 5

Open the Upload.cshtml web page file in WebMatrix and replace this header element:

HTML
<h1> Upload Lost Cat Photo </h1>

with:

HTML
<h1>Upload Lost Cat Photo</h1>

<p>
    The photo or your lost cat that you upload will be placed in the
    <a class ="italic" href ="~/View/ @galleryId " title ="@ gallery.Name"> 
     @gallery.Name </a> gallery.
</p>
<p> The photo needs to be a picture of your cat looking at the camera, 
    preferably straight ahead and not looking up, down, or to the side.</p>

Step 6

Add the following snippet to View.cshtml which is found in the Photo folder, the web page that displays a single photo. It adds the KittyDar support scripts to this page:

Java
@section Head {
    <!-- KittyDar cat face detector script files: https://github.com/harthur/kittydar -->
    <script src ="~/Scripts/kittydar-demo.js"></script>
    <script src ="~/Scripts/kittydar-0.1.6.js"></script>
    <!-- CSS to format and align properly the elements used to show the KittyDar operation
         annotations and result. -->
    <link rel ="stylesheet" href ="kitydar.css"/>   
}

Step 7

Find the image element for the large photo. Replace the existing IMG tag element with the snippet below. This snippet is where the photo is displayed and also the bounding boxes and annotations generated by the KittyDar detection engine. As KittyDar identifies the cat's face, you will see bounding boxes drawn on the image canvas until the face is found and the final bounding box is drawn, or the operation fails and the canvas will be cleared of bounding boxes. These elements are managed by the kittydar-demo.js and kittydar-detection-worker.js scripts. The inline HTML comments explain what each document element does:

HTML
<!-- <img class="large-photo" alt="@Html.AttributeEncode(photo.FileTitle)" 
      id="kittydar-viewer" src="@Href("~/Photo/Thumbnail", 
      photo.Id, new { size="large" })" /> -->
    <div id ="viewer-container">
        <!-- This is where the uploaded cat photo is displayed. 
             It is also where any bounding rectangles that display the cat face 
             detector's progress and final result are displayed -->
        <div class="kittydar-viewer" id ="kittydar-viewer"></div>
        <div id="viewer">
            <canvas id="preview">
            </canvas>
            <canvas id="annotations">
            </canvas>
        </div>
        <!-- This is where progress messages from the KittyDar detector 
             are displayed as it searches for a cat face in the uploaded picture. -->
        <div class="kittydar-progress" id ="kittydar-progress">(none)</div>
    </div>
    <div id ="detection-result">
        <!-- This is where the result of the detection will be displayed, 
             whether it succeeded or failed. -->
        <div class="kittydar-result" id ="kittydar-result">(none)</div>
    </div>

Step 8

Add the following snippet to just before the ending closing DIV tag. It will run the KittyDar cat face detector on the currently displayed photo:

JavaScript
<script> 
// Run the KittyDar cat detector on this photo
detectFromUrl("@Href( "~/Photo/Thumbnail", photo.Id, new { size="large" })");
</script>

Step 9

Add the following snippet to the _SiteLayout.cshtml file to make sure our custom HEAD elements in the view page are rendered:

JavaScript
<!-- Render the head section if defined, like it is in View.cshtml -->
@RenderSection( "Head", required: false);

Step 10

Add kittydar.css to the Content folder. It will make sure the KittyDar view elements are formatted propertly.

Step 11

Publish your web site to Azure using the WebMatrix 3 Publish button. You're done!


Short Form Explanation

NOTE: If you followed the Long Form Explanation steps to modify your site, you do not need to follow these steps and can skip this section.

Step 1

Follow the instructions in this tutorial to get the WebMatrix 3 Photo Gallery template up and running, but SKIP the CAPTCHA setup steps since the file containing the CAPTCHA codes is about to be overwritten in the next step:

WebMatrix 3 Photo Gallery Tutorial

Step 2

Make a backup of your WebMatrix 3 project files now. Then, download the KittyDar-Photo-Gallery.zip file. Open the compressed folder and select all the files and directories contained and copy them into the root directory of your web site that you created with the WebMatrix 3 Photo Gallery template. The destination directory should contain the file Web.config, that's how you know you have the right target directory for the copy operation.

Step 3

Go back to the Photo Gallery tutorial and return to the section titled "Enabling CAPTCHA in the registration page". Follow the instructions to setup CATPCHA properly on your site.

Step 4

Publish your web site to Azure using the WebMatrix 3 Publish button. That's it! Now test your web site and make any desired changes you need to make.

Test Your Web Site

  1. Run the project.
  2. Register for an account if you have not done so already. This will be your test account.
  3. Login to your test account.
  4. Create a New Gallery called "Lost & Found".
  5. Select the gallery and open it.
  6. Click on "Upload a Photo" to begin the real test.
  7. Use the "Choose File" button to select a file that contains a picture of a cats face to upload. If you don't have any cat pictures of your own, you can find plenty of public domain cat photos to test with at this link:

Public Domain Cat Photos

This, in particular, is a good test photo:

Contented Cat

Make sure you have the rights to any photos you upload or that they are in the public domain since the photos will be publicly available on the Web!

Easter Egg

To see the Easter Egg on FindAPet, view any photo that contains more than one detected cat face on the main viewing page. Better yet, just visit the link below:

Spooky cats!

Resources

WebMatrix 3

Azure

KittyDar

Phase 3 - The Wish List, an SQL Table Hosted by Azure

In keeping with the theme of Phase 2, my mission with this segment of the Azure Developer Challenge is to show just how easy and fast it is to use Azure to do critical database tasks, in this case, the task of creating an SQL table that lives in the cloud. This is a fast tutorial that shows you how to create a simple table in an Azure SQL database, but also how to integrate and synchronize your database management efforts between Azure and a local database project in Visual Studio. You will also find a few important tips and warnings to make sure your experience is a smooth one.

The Adoption Wishlist

In the final phase of this contest, I will be covering the user's main point of access to the Azure Animal Adoption Agent, a Windows Phone application. From that application, they will search the PetFinder inventory of adoptable pets, an inventory comprised of the pets available from a vast network of participating animal shelters throughout the USA. As they browse adoptable pets close to their current location, they can add pets that they like to their wish list.

A Wishlist in the Clouds

As described in the Phase 1 segment of this article, the overall system will alert a user if any of the pets that they have added to their wish list are scheduled to be put to sleep soon. If there are pets in danger, an e-mail will be sent to the user so they can run down to the animal shelter and adopt the pet before it's too late. This mandates that the Wishlist must be located in the cloud instead of on the user's phone, so that a persistent task running on the Azure server can periodically scan the wish list database for pets in danger. It will then generate an e-mail to alert the proper users of this dire occurrence so they can get down to the shelter and adopt the pet quickly.

Creating the Wish List SQL Table

Log in to the Azure Portal and select the SQL Tables option:

New SQL Table

Click on Create A SQL Database to create a new database.

You will see the Settings screen. Enter the name of your database and adjust any other settings as you see fit. I just used the defaults and named my SQL database findapet_wishlist:

Database Name

Azure will show you the list of active SQL databases now. As you can see, the only database I have is the findapet_wishlist database I just created:

Now I will add the wish list table to the database. Click on the findapet_wishlist database row and bring up the SQL Database Design wizard:

SQL Database Design Wizard

Click on Download a starter project for your SQL database to download the project Azure has prepared for you that will make working on the table remotely from Visual Studio easier. Run Visual Studio and open the SQLDataProj.sln file. Then double-click on the file named SQLDatabaseProj.publish.xml found in the Solution Explorer pane. You will see the screen below:

NOTE: If you do not have a version of Visual Studio that has the SQL Server Data Tools (SSDT), download and install them now from this web page. If you do not have the SQL menu option in the top level menu bar in Visual Studio, as shown in the figure below, then you need to download them from that web page.

Registering the database as a Data-tier Application and enable database drift detection

I chose to register the database as a Data-tier Application (DAC) by checking the appropriate box for that. This makes it much easier to move databases around between servers for load balancing and other useful purposes. DACs are great for simple databases like the wish list database but can be problematic for more complex databases. This article goes into depth about DACs so you can make the decision that is right for you. Later in this segment of the article, I will talk about SQL Server Data Tools (SSDT) to manage databases. By checking the Block Publish option, I can have SSDT check to see if the structure of the data has drifted from the DAC image of the schema and if so, any attempt to publish from Visual Studio will be blocked with a warning indicating this condition. This is a useful feature to prevent improper updates or database corruption from occurring if the image of the database on the server does not match what we have in our DAC image in Visual Studio. Further details on SSDT, DACs, and database drift can be found in this article.

The screen below shows my changes:

IP Address firewall settings

It is necessary to tell Azure that our current IP address is valid for administering the database. If you do not set up a firewall rule, then anytime you attempt a remote operation like publishing the database, etc. you will see an error like this one:

Firewall Warning

Fortunately, Visual Studio and Azure make it very easy to update the firewall rules to accept your current IP address. Go back to the Azure management portal and click on Set up Windows Azure firewall rules for this IP address. You will see the following screen from the Azure management portal (with your actual IP address shown of course):

Just click Yes and the firewall rules for your database will be updated to accept your IP address.

Database login settings

Return to Visual Studio and the Publish Database dialog box. By clicking on the Edit button in the Publish Database dialog, you will open the Connection Properties dialog and can provide the login settings for the database. Using the dialog shown below, provide the correct user name and password that you set using the Azure management portal:

Database login settings

It is a good idea to click the Test Connection button to make sure your login settings and IP firewall settings are proper or not. Once you have validated them, you are ready to publish your database.

Test connection succeeded

Click OK to dismiss the results dialog box and then click OK again to dismiss the Connection Properties dialog box. You will find yourself back at the Publish Database dialog. Click the Publish button to publish the changes to Azure. When it's done, the Data Tools Operations pane should show the following progress and result messages:

Adding Tables, Columns, and Indices

Now that the wish list table is live in Azure, it is time to add the wish list table, define its fields, and define the indexes for the table. Important! Instead of using SSDT for those operations, I am going to do something unorthodox and use the Azure management portal for that purpose instead. I am doing this to show you an important SSDT feature for synchronizing remote and local databases, so please follow along.

NOTE TO CHROME BROWSER USERS! In Chrome, I did not see the popup that Azure pops up when trying the Design your SQL Database or Run Transact-SQL queries against your SQL database options on the New SQL Database wizard screen. I did not get a popup blocked warning either. I had to switch to Internet Explorer to complete the following steps. Then I did get a popup warning from IE, so I chose the "Always Allow" option, and from then on was able to execute the design tasks successfully.

Return to the Azure management portal and select the Design Your SQL Database option underneath the Connect to your Database sub-heading, as shown in the figure below:

Design your database

Azure will ask you to login to the database as shown in the figure below. Enter the user name and password you assigned to the database and login now:

Database login

You will see the database design screen as shown in the figure below:

Database design

Click on New Table to create the main table for the database. Name the table wishlist. Then, add 6 new columns and make sure they match the definitions shown in the screen below. Make sure the ID column is marked as being the Primary key and that all fields are marked as required.

NOTE: If you are concerned with being able to scale your database easily on Azure, then don't mark any of the columns with the Identity attribute since SQL Azure Federations don't support Identity columns. For more information on SQL Azure Federations and scaling, visit this web page for more information.

Your column definitions should look like this now:

The reason for adding the pet_name and shelter_name fields is to speed up most of the display operations we will do in conjunction with the wish list table. With those fields present in the table, we don't have to make additional calls to the PetFinder API server to get those items. Many PetFinder API methods just return the ID elements and you have to make a different method call to get the associated names. This tactic cuts down on the number of calls we make to the PetFinder API, and that helps to keep us below their API's rate limits.

Now click on Indexes and Keys to define those items. Add two indexes as follows:

  1. Click on Add an index. Add an index named IX_wishlist_user_email that indexes the user_email column.
  2. Click on Add an index again. Add an index named IX_wishlist_shelter_id that indexes the shelter_id column.

If you succeeded, your screen should now look like the one shown below:

Indexes and Keys

The user_email column is indexed so we can pull up all the pets for a particular user using their e-mail address as the key. That allows us to make fast queries to find the set of records that make up one user's wish list. The shelter_id column is indexed so we can grab all the animals that belong to one particular shelter across all the user's wish lists, to make the task that scans for animals that will be put to sleep soon faster and easier to execute. When you are done, click the Save icon to save the changes.

Synchronizing Local and Remote Database Schemas

By changing the remote database schema directly, we have created a problem. The database schema we published from Visual Studio using SSDT was an empty database. Since the remote database now has a new table with a complete set of columns and indexes, the Azure data schema and our local database schema no longer match. They are out of sync! Fortunately, SSDT gives us an easy way to synchronize them. I did this on purpose just so I could show you how to use SSDT to synchronize the two database schemas.

SSDT can compare two database schemas and report the differences between them. We are going to do that now. Click the SQL menu option at the top of the Visual Studio IDE, select Schema Compare, and then New Schema Comparison as shown below:

This creates a new schema comparison as shown below:

Schema Comparison

Since the Azure database image is newer, due to our recent modifications, we need to designate it as the Source for our comparison. Click the SQL menu option at the top of the Visual Studio IDE, select Schema Compare, and then Select Source as shown below:

Use the Select Source Schema dialog as shown below to choose the Azure database as the source for the comparison:

Select source dialog

After clicking OK, it is time to select the target for our comparison. We want our current Visual Studio SQL database project be the target, since we want to propagate the changes we made from the remote Azure database to our local database project. Click the SQL menu option at the top of the Visual Studio IDE, select Schema Compare, and then Select Target as shown below:

Select target for comparison

Select the Visual Studio SQL database project as the target for the comparison using the Select Target Schema dialog shown below. Our SQL database project is named SQLDatabaseProj:

After clicking OK, it is time to do the comparison. Click the SQL menu option at the top of the Visual Studio IDE, select Schema Compare, and then Compare as shown below:

SSDT performs the comparison and shows you the differences between the two schemas as you can see in the figure below. In the left half of the Results pane are the SQL instructions for creating the wish list table, the fields we added, and the indexes too, since all those instructions are needed to make our local SQL database project look like the remote Azure database. The right half is blank because our SQL Database project currently only has the database definition and nothing more. Above the Results pane is the Actions table. It shows you what actions will be taken when the Update button is clicked. There is only one row in this table to add the wish list table and its indexes to the target. The Action checkbox is currently checked because we do want to add the table and its indexes to the local database project. In more complex scenarios, there could be multiple rows of different actions necessary to synchronize the two database schemas. The Action checkbox allows you to decide which actions you want or don't want to take during the update.

Click the Update button. Visual Studio executes the update and then prompts us to do another Compare to refresh the schema comparison screen. We do that and the comparison screen changes to look like this:

The screen shows us that we have successfully synchronized the two databases and there are no differences between them.

Conclusion

Azure by itself is a powerful, capable database manager. But the combination of Azure and Visual Studio provides local and remote database features that make SQL database management robust, professional, and easy. Not only did we rapidly create the core database, table, columns, and indexes necessary to service the FindAPet wish list feature, but we also created a local Visual Studio project that we can use to help move databases between servers for scaling and other important purposes. Finally, we used the SSDT toolkit that integrates seamlessly with Visual Studio to synchronize effortlessly our local SQL database project with the structure of the remote database living in the Azure cloud. SSDT gives you a potent method to manage local and remote databases in a robust way that guards against the dangers of database drift, a phenomenon that frequently creeps into even the most well-managed of database projects.

NOTE: For an in-depth discussion of the many options and capabilities that SSDT provides for doing a database schema comparison, read this article:

How to Use Schema Compare to Compare Different Database Definitions

If you do not have a version of Visual Studio that has the SQL Server Data Tools (SSDT) already in it, download and install them now from this web page.

Phase 4 - A Virtual Assistant on a Virtual Machine

Most of your web presence needs can be satisfied by Azure's web site, worker role and other common services. But there are times when the only valid solution to offering a service on the web is a virtual machine (VM). A perfect example of such a time is when you have a standalone program that acts as a server, and a great example of such a program is a Chatbot. A successful chatbot creates the illusion of conversing with another person, when in reality you are talking to a computer that uses sophisticated pattern matching algorithms to analyze your input and then returns a reasonable response. In this phase of the contest, we will provision an Ubuntu Linux VM on Azure. We will use that VM to run a chatbot that explains how to use the web site we created in Phase 2 to our site visitors.

Talk to the Lost & Found ChatBot

Why Linux?

The chatterbot I chose to use is ChatScript, a battle-tested and powerful open source implementation written by artificial intelligence expert Bruce Wilcox that has won the Loebner Prize for best chatbot on several occasions and is used by several popular web sites for their virtual assistants. It's distributed under a liberal open source license that lets you do pretty much anything you want with it without requiring you to distribute your source code. In the server document for the software (ChatScript Server Manual.pdf), there is a crucial paragraph on performance and different operating systems:

"The fastest server OS for ChatScript is Linux. The Mac tends to misfire in the OS itself with heavy client loads. Windows is a much slower server in general. And the Linux version of ChatScript has support for forking ChatScript (no such support under Windows), so you can run a fork of the engine on every core, saturating cpu processing to the max while still serving a single port. Speedup is nearly linear per core added."

Since Linux also has a significantly smaller memory footprint than Windows, you can run a Linux ChatScript server on an Azure Extra Small Compute instance! Given Azure's courtesy usage levels, this means you essentially get to run your chatbot for free! If you start to get really heavy levels of usage, you can just upgrade your VM to a more powerful compute instance. One of the beauties of Azure is that it can run a Linux VM as effortlessly as a Windows VM. (Note: if you really want to use Windows to run ChatScript, you can. It will run fine. But this article focuses on the solution that gives the best performance, both computationally and economically.)

Step 1 - Provision the Linux VM

Setting up a Linux VM on Azure is easy. Rather than reinvent the wheel, this tutorial will have you up and going with an Ubuntu 12.0.4 LTS VM in a few minutes.

Important notes on following the tutorial!:

  • When you get to the screen where you select the SIZE for your VM, make sure you select Extra Small (Shared core, 768 MB Memory), instead of the author's choice of Small.
  • You do not need RDP (Remote Desktop Protocol)!

Just follow the first two parts of the article that shows you how to set up the VM and configure it. Then come back here when you reach the third part that tells you how to install RDP support. RDP is a great tool for taking remote control of a Linux box, but it has crucial security implications so it must be configured carefully and it can cause a big increase in your bandwidth consumption. You do not need RDP at all to successfully manage your VM running ChatScript. Pay special attention to the helpful tip Part Two of that tutorial has on setting a root password.

Step 2 - Install BitVise

A great tool for managing your Linux VM is BitVise, an open source app you can use for SSH sessions and has a Windows style File Explorer for transferring files between your Linux VM and your local PC. It's free for non-commercial personal use, otherwise you need to buy a license. Download and install it now. You will need it to manage your Linux VM. However, experienced Linux users will already have their own tools to manage a Linux VM and can skip this step.

Now run Bitvise. The first thing you need to do is configure the Host and User Login Details necessary to connect remotely to your Linux VM, using the Login tab on the initial Bitvise screen. The Host field should contain the DNS you chose for your VM during the VM tutorial in step one. The Username and Password fields should contain the user name and password you chose for your VM in the VM Configuration screen during the tutorial. The Initial Method field should be set to password. When done, the Bitvise Login dialog should show values similar to those below, but with your particular values of course. Whether or not you choose to save the password associated with this login as I did is up to you:

Bitvise configuration

Now click the Login button to connect to your Linux VM. Once you are connected, Bitvise will automatically open an Xterminal window so you can enter commands via an SSH session, and a File Explorer window so you can manage your files securely with SFTP.

TIP: I found it useful to change the width of an Xterminal window from the default of 80 characters to 160 characters. You can do this from the Terminal tab in the Bitvise main screen. Just look for the screen width field on that tab.

Step 3 - Install the Ubuntu 32-bit Compatibility Package

Azure installs a 64-bit version of Ubuntu. ChatScript comes with a pre-built Linux executable called LinuxChatscript32. It is a 32-bit program and will not run on a 64-bit VM without some help. Switch to the Xterminal window that Bitvise opened for you automatically and enter this command:

sudo apt-get install ia32-libs

Enter your superuser password as required by the sudo command. After answering the confirmation prompt, it will take about 5 to 10 minutes for the library to be installed. When it's done, your VM is now ready to run 32-bit programs like ChatScript.

Step 4 - Download ChatScript

Download ChatScript and extract the contents of the compressed archive to a directory of your choosing on your local PC. That directory will be your local install directory.

Step 5 - Transfer ChatScript to the Linux VM

Switch to the File Explorer window Bitvise opened for you. The left pane (Local Files) contains your local PC files and the right pane (Remote Files) contains the files on the Ubuntu instance running in a VM. In the Local Files pane, navigate to the directory that contains the extracted contents of the ChatScript 3.1 zip file. Then navigate to the parent of that directory so you can see the top level ChatScript directory. The Remote Files pane should already be set to the top level directory for the user you configured to manage the VM. Drag the ChatScript directory from Local Files over to Remote Files so that the entire file structure is replicated into the Ubuntu instance:

Transfer ChatScript

Once you have done this, Bitvise will begin the file transfer and display a message in the status bar that says "Preparing upload list.." for a few minutes. Then it will display the ongoing status of the transfer. With my ISP, the transfer took about 22 minutes so get a sandwich or write another article for Code Project while you wait for it to complete:

File transfer in progress.

Once the file transfer has completed, you need to tell Linux that the file named LinuxChatscript32 is an executable program, by setting its file permissions as follows:

  • Navigate into the ChatScript sub-directory that should now be visible in the Remote Files pane by double-clicking on it.
  • Find the file named LinuxChatscript32.
  • Right-click on it and choose Properties from the pop-up menu.
  • Select the Permissions tab.
  • Make sure you have checked the boxes so that the permissions for LinuxChatscript32 match those in the screen below:

The ChatScript server is now ready to run.

Step 6 - Making ChatScript Robust

You want the ChatScript server to be up and running at all times, or close to it. If it crashes, you want the Linux VM to automatically restart it without requiring your intervention. Linux has a background process called cron that is used to launch background tasks at predetermined times and is installed by default in an Azure Ubuntu Linux VM. To accomplish this goal, we make an entry into the cron table that tells the Linux VM to launch ChatScript every 5 minutes. ChatScript will simply exit if the port it uses to listen for connections is already opened. Therefore, if ChatScript has crashed, then it will be reloaded when cron launches it at the next 5 minute interval. If it is already running, then ChatScript simply exits. The net result is an automated method that keeps our ChatScript running nearly all the time. If ChatScript crashes, then at most we will be without its services for 5 minutes or less.

Here's how to make the cron table entry that does this. Switch to the Xterminal window again. At the prompt enter:

crontab -e

Then do the following:

  • Accept the default choice of Nano as the editor to use for editing the cron table.
  • Copy the line below to the clipboard, followed by right-clicking on the Xterminal system icon in the upper left hand corner, and then choosing Edit -> Paste:
    0,5,10,15,20,25,30,35,40,45,50,55 * * * * cd /home/[username]/ChatScript; 
    ./LinuxChatscript32 2>/home/[username]/cronserver.log 
  • Replace [username] with the user name you assigned to VM Configuration dialog during the Linux VM tutorial.
  • Now type Ctrl-X to save the file.
  • Type Y to accept the save
  • Click Enter to accept the crontab file name offered to you.

You will see a status message telling you that the cron table has been installed. Now, every five minutes, cron will change to the top level directory for ChatScript, and execute LinuxChatscript32, the program that is the ChatScript server. Any errors that occur during the attempt will be written to the cronserver.log file in the given directory. You can easily see the contents of your cron table with the following command, which will dump the current contents of the cron table to the screen (note, the character after the dash is a lowercase "L"):

crontab -l

To look at the contents of the cronserver.log file, go to the top level directory for ChatScript while inside the Xterminal window and enter the following command:

cat cronserver.log

That will dump the contents of the log file to the screen. You can easily test to see if your cron table entry is working by waiting 5 minutes, and then executing the following command in the Xterminal window:

netstat --listen

This command will show you all the ports that running processes are listening on. You should see that port 1024 is in use. If not, recheck carefully your steps during the cron table editing operation above for errors, especially for typos in any path entries. You can also look at the file /home/[username]/cronserver.log and see if any errors occurred during the cron job. (Note: ChatScript listens on port 1024 by default, but you can change that if you like via the configuration files. See the ChatScript server manual for details.)

Step 7 - Making ChatScript Available to the Web

We need to tell Azure to route traffic from the Web to the internal port that ChatScript is listening on. This is done by setting up an Endpoint with the help of the Azure Management Portal. Follow these steps now:

  • Log into the Azure Management Portal
  • When the portal is fully ready, click on the Virtual Machines icon
  • Select the Ubuntu Linux VM you created from the list
  • Click on the ENDPOINTS option to see the current list of Endpoints
  • Click on the ADD button to add a new Endpoint
  • Accept the default ADD ENDPOINT option as shown in the figure below by clicking on the right-facing arrow that takes you to the next step:

In the dialog that appears, give the new Endpoint the name ChatScript while keeping the default value of TCP for the protocol. Set both the PUBLIC PORT and PRIVATE PORT fields to 1024. the port that ChatScript listens on for new connections. Your screen should look like the figure below:

Click on the checkmark icon to go to the next step. You will see a status message saying UPDATE IN PROGRESS while Azure adds your new endpoint. This will take a few minutes. When it is done, your Endpoint list screen should look like the following:

Endpoint added

Step 8 - Restricting Access to ChatScript's Meta-Commands

ChatScript has commands that can be issued remotely to the server, directly from a chat. They all start with a colon (":") and they usually have a dramatic affect on the ChatScript server such as completely reloading it, adding new content, and more. They should only be used by you or someone you designate as a system administrator. They absolutely should not be available to everyone that visits your chatbot!

There is a file in the top level ChatScript directory called authorizedIP.txt. This file should be edited to contain only the IP addresses that should be allowed to issue meta-commands. The file ships with the word "all" as its sole contents which is dangerous. While at the PC that you will use to administrate the ChatScript server, visit a site like What Is My IP to discover your IP address, and enter that into the authorizedIP.txt file. Repeat this procedure for each PC you want to grant access to, if they are at different IP addresses. You can use the Nano editor to edit the file, the same editor you used to edit the cron table. To perform the edit, switch to the Xterminal window, make sure you are in the top level ChatScript directory, and use the following command:

nano authorizedIP.txt

Make sure you delete the "all" keyword first.

IMPORTANT!: Do not add the IP address belonging to your web site that is talking to the ChatScript server! Since the web site is acting as a proxy to the ChatScript server, you would effectively be granting everyone meta-command rights! Therefore, the only time you will be able to issue meta-commands is when running your web site from a local copy of IIS that is running on the PC whose IP address you added to authorizedIP.txt, since the requests your web site make in that context originate from that PC's IP address. For WebMatrix users, this is the context your web site runs under when you press F12 to launch the web site locally. This is contrast to ChatScript server requests your public web site make, which originate from your public web server's IP address.

Step 9 - Wrapping the ChatScript TCP Request in an HTTP GET Request

The ChatScript server does not respond to HTTP requests, it only accepts TCP socket connections on the port it is listening on. Although ASP.NET web sites can open sockets directly, it is much more convenient to use AJAX requests, especially when it comes to crafting a nice web page that interacts with the user while proxying the ChatScript server behind the scenes. Unfortunately, due to browser security restrictions on JavaScript AJAX requests only work with HTTP requests. The trick then is to create a web page that can accept an HTTP GET request, make the TCP connection to the ChatScript server, issue the chat request using the given GET parameters, and return the server's response as a typical response page. That is precisely what the file ChatRelay.cshtml does.

NOTE: I have added a mini-build of the web site files called ChatEssentials.zip that contains _SiteLayout.cshtml, ChatMessage.cs, ChatRelay.cshtml, and Chat/Default.cshtml, all discussed below except for _SiteLayout.cshtml. The only modification made to _SiteLayout.cshtml is to add a site-wide "Help" link that takes a visitor to the chatbot page. There is a readme.txt file in the zip file that tells you how to use the files to update your copy of the KittyDar Photo Gallery to add a chatbot, or how to use the included files to add a chatbot to your own unique web site that has nothing to do with the Lost & Found photo gallery. These files should work fine for ASP.NET web sites that were not created with WebMatrix 3 too.

A client request to the ChatScript server consists of sending 3 null terminated strings to the server. Here's the code from ChatMessage.cs that shows the properties for the class ChatMessage that embodies a ChatScript server request:

C#
public class ChatMessage
{
    public static string NULL_TERMINATOR = "\0";

    public string LoginName { get; set; }
    public string BotName { get; set; }
    public string Message { get; set; }

    ...
} // public class ChatMessage 

The LoginName is the unique ID you assign to the user. This is necessary so that the chatbot can remember information for a particular user between sessions. That is, between separate visits by a user to the chatbot web page. The BotName is the name of the bot to use. ChatScript can support multiple chatbots, each with their own "personalities", fact sets, and conversation rules. It can be left empty to use the default chatbot. The Message is the text the user entered that requires a response from the chatbot.

NOTE: The only time the Message field should be blank is when a new session started. That is, when a user enters the web page to talk to the chatbot, even if it's a return visit. This lets the chatbot know that a new session has started. However, while the user stays on the web page to chat with the chatbot, you should not send an empty string to the chatbot! If you look at Chat/Default.cshtml, the web page I use for facilitating a chat, the first thing I do when the web page loads is to send a chat request with an empty message. From that point on. a warning message is shown to the user if they try to submit a chat request with an empty chat message, preventing that from happening.

Along with other helpful methods, the ChatMessage class contains ToString(). This method builds a string that you can send directly to the chat server with the chat message properties properly formatted with C string NULL terminator characters:

C#
/// <summary>
//  Returns a string formatted properly for acceptance by the ChatScript server.
/// </summary>
/// <returns></returns>
public override string ToString()
{
    // The ChatScript server expects 3 C-style strings terminated
    //  with the null terminate character.  Ascii(0)
    //  The 3 strings are the login name (user ID), the chatbot name,
    //  and the last message from a current
    //  conversation, or an empty string if this is the
    //  first message for a new session, where a session is
    //  defined as the user "entering" the web page to chat with the chat bot.
    return this.LoginName + NULL_TERMINATOR + this.BotName +
                            NULL_TERMINATOR + this.Message + NULL_TERMINATOR;
} // public string ToString()

This class also contains a handy method name readChatScriptMessage() that reads the response from the ChatScript server from an open socket connection and returns it as a string. The ChatMessage class is utilized by ChatRelay.cshtml to talk to the ChatScript server. ChatRelay.cshtml takes 3 GET request arguments that correspond directly to the three properties in the ChatMessage class, and have the exact same name for simplicity. Here's a sample GET request using the FindAPet web site:

http://findapet.azurewebsites.net/Chat/ChatRelay.cshtml?LoginName=192_58_16_17&BotName=&Message=I%20lost%20my%20cat.%20%20Can%20you%20help%20me?

This request tells ChatRelay.cshtml to ask the chatbot the question "I lost my cat. Can you help me?". Currently, the user's IP address with periods being substituted by underscores is used as their ID. This is a flawed approach since they could be sharing the IP address or have a dynamic IP address, but it makes it easy to deal with guest users. The risk is that information memorized by the chatbot will be mixed between people using the same IP address, or lost if a user revisits the web site with a new IP address, but for now it's a quick and dirty solution for easy guest visitor access. A better solution would be a cookie based approach but that fails too if they block or reject cookies. The best solution is requiring a user account with a verified e-mail address, but that would require that they create an account just to interact with the chatbot. Pick your poison and run with whatever approach suits your web site the best.

The first thing ChatRelay.cshtml does is get the IP address for the Linux VM so we can use it to open a socket connection. (Note: Instead of [azuredns], you would put the DNS name you assigned to your Linux VM when you created it):

C#
// Lookup the IP address for our chatscript server. (Cache this value
//  in a later build since GetHostEntry() is reportedly a slow call.)
IPAddress ipAddress = Dns.GetHostEntry("[azuredns].cloudapp.net").AddressList[0];

Then the URL arguments from the GET request are validated. The LoginName is required, but the BotName and Message arguments are not. Note, checkForValidURLArgument() is a simple method that validates a URL parameter and throws an Exception if it is invalid and the parameter is one that is marked as required. You will find it and several other helper methods in ChatRelay.cshtml:

C#
// LoginName, is mandatory.
strLoginName = checkForValidURLArgument("LoginName", true);

// BotName, is optional.
strBotName = checkForValidURLArgument("BotName", false);

// User message (chatbot input), is optional.  But remember,
//  only send a blank message to start a new session
//  with ChatScript!  After that, send the user's input
//  each time.
strMessage = checkForValidURLArgument("Message", false);

Once the parameters are validated, it is time to connect to the ChatScript server, create a chat message object using the given GET request parameters, send the request to the chatbot, and wait for a response:

C#
// Connect to the ChatScript server.
TcpClient tcpCli = new TcpClient();
tcpCli.Connect(ipAddress, 1024);

// Open the stream
streamChatScript = tcpCli.GetStream();
StreamReader sr = new StreamReader(streamChatScript);
BinaryWriter sw = new BinaryWriter(streamChatScript);

// Create a message to send to the server, using the URL argument values
//  passed to us.
ChatMessage cm = new ChatMessage(strLoginName, strBotName, strMessage);

// Send the message to the chat server.
string strSendChatMsg = cm.ToString();

// Translate the passed message into ASCII and store it as a Byte array.
Byte[] data = System.Text.Encoding.ASCII.GetBytes(strSendChatMsg);

for (int i = 0; i < strSendChatMsg.Length; i++)
{
    data[i] = (byte)strSendChatMsg[i];
}

// Send the chat message.
streamChatScript.Write(data, 0, data.Length);

strResponseMsg = ChatMessage.readChatScriptMessage(streamChatScript);

The guts of the web page simply displays the response, wrapped XML style in a tag named response by the formatResponse() method:

HTML
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
    <!-- Just output the response message from the C# code above -->    
    @strResponseMsg
    </body>
</html>

A sample response from the ChatScript server, returned inside the rendered web page could look like:

HTML
<response>Do you have a photo of your cat?</response>

Remember, the user never sees this output. Only the web page that interacts with the user and makes a behind the scenes AJAX request to ChatRelay.cshtml sees it, as described below.

Step 10 - Chatting with the User

The page that interacts with the user is called Chat/Default.cshtml. You can use it as a starting point for your own web site. With a few small modifications, you should be good to go. It's time to explain how the web page that interacts with the user and the ChatScript server works. You can talk to the Lost & Found ChatBot now using the link below:

Talk to the Lost & Found ChatBot

Below is a screenshot of the chat page:

Chatbot page

This is the web page named Chat/Default.cshtml. The active elements that comprise the web page are contained within the DIV that has the ID "#q_and_a". It contains a text area with the ID of "#entry", a submit button with the ID "#do_chat", a DIV with the ID "#user_question" and another DIV with the ID "chat_response":

HTML
<div id="q_and_a" >
<br />
<div id="user_input" class="q_and_a_element" >
    <h3 class="label">Enter your question or reply in the box below.</h3>
    <textarea id="entry" onkeypress="if(event.keyCode==13) return doChat();" rows="2" cols="" name="entry"></textarea>
    <br>
    <input type="button" value="Ask" id="do_chat" name="do_chat" onclick="doChat()" >
 </div>
 <div id="you_said" class="q_and_a_element" >
     <div class="label">You said:</div>
     <div id="user_question" class="q_and_a_text_field" >
         <!-- When the user hits the submit button, their question will be echoed here. -->
         &nbsp;
     </div>
     </div>
 <div id="i_replied" class="q_and_a_element" >
     <div class="label">I said:</div>
     <div id="chat_response" class="q_and_a_text_field" >
         <!-- When the user hits the submit button, the response from the ChatScript server will be placed here. -->
         &nbsp;
         </div>
    </div>
</div>

A small piece of JQuery JavaScript adds a document ready event handler that calls the JavaScript method doChat() with the bFirstMessage parameter set to TRUE. As described previously, this sends a chat request to the ChatScript server with an empty Message field, the only time this happens during a chat session. This lets the chatbot know that this is the start of a new session or conversation with the current user:

JavaScript
<script>
    // When the page is loaded, send a chat with an empty message to let the ChatScript
    //  server know this is a new session for the current user.
    $(document).ready(function () 
    {
        // Set the bFirstMessage parameter to TRUE so the chatbot knows 
        // this is the start of a new session.
        doChat(true);
    });
</script>

The chat session is now in progress and from this point on, the following pattern repeats until the user leaves the chat page. The user enters their question or response in the "#entry" textarea and then hits the Enter key or clicks the Ask button. This triggers a call to the method doChat() that does all the work. In this case, bFirstMessage is omitted resulting in that parameter being set to FALSE. If the user neglected to enter anything into the "#entry" textarea, an alert box will pop up telling the user to enter something and the chat request will not be submitted:

JavaScript
// This method is called when the user hits the ASK button. The user's current message
//  is sent to the ChatScript server and the server's response is shown in the "You said"
//  DIV element.
//
// PARAMETERS:
//  bFirstMessage - If TRUE then an empty message is sent to the ChatScript server to
//   let it know this is a new chat session for this user.  If FALSE, then an empty
//   message will trigger an alert telling the user to enter a valid message.
function doChat(bFirstMessage)
{
    // The default value for bFirstMessage is FALSE.
    typeof bFirstMessage == 'undefined' ? false : bFirstMessage;

    var strGuestID = "@strGuestID";
    var strMessage = "";

    if (bFirstMessage)
    {
        // First message.  Send an empty message but show the word "Hello"
        //  as the user's question.
        $("#user_question").html("Hello.");
    }
    else
    {
        // Not first message.  Look for the user message in the message entry text area.
        strMessage = $("#entry").val().trim();

        // Empty messages are not allowed if this is not the first message
        //  for the session..
        if ((!bFirstMessage) && (strMessage.length < 1))
        {
            alert("Please enter a question or statement first.");
            return;
        }

        // Show the question.
        $("#user_question").html(strMessage);
    } // else - if (bFirstMessage)

    // Build the URL and URL arguments to make the request to the ChatScript server
    //  via the ChatRelay page.
    var strUrl =
        "/Chat/ChatRelay.cshtml"
        + "?"
        + "LoginName=" + encodeURI(strGuestID)
        + "&"
        + "BotName=" + encodeURI("")
        + "&"
        + "Message=" + encodeURI(strMessage);

    $.ajax(
        {
            url: strUrl,
            // Do not cache the results of a query.  We want a fresh response
            //  from the ChatScript server each time.
            cache: false
        }).done(function (html) {
            var strResponse = "(none)";

            // Wipe the text area of the user's last question
            // so they can enter something new.
            $("#entry").val("");

            // Extract the response contained in our "response" XML tags.
            var re = /&lt;response&gt;(.*?)&lt;\/response&gt;/im;
            var match = re.exec(html);

            if (match != null) {
                strResponse = match[1].trim();
            }
            else {
                strResponse = "Chat is offline at the moment.
                               Please try again in 30 minutes.";
            } // else if (match != null)

            // Show the response returned by the ChatScript server.
            // $("#chat_response").val(strResponse);
            $("#chat_response").html(strResponse);

        });
} // function doChat()

The first thing doChat() does is build the GET request with the proper URL arguments to send to the chatbot:

  • LoginName is set to the user's IP address properly formatted.
  • BotName is left empty since we are using the default chatbot.
  • Message is set to the text just entered by the user

First, the user's recent input is copied into the "#user_question" DIV. Next, an AJAX request is crafted with caching set to FALSE so we always get a fresh response from the chatbot, and an anonymous completion handler is defined that does the following (attached to the JQuery AJAX done() event):

  • Clears the "#entry" textarea to make way for new input from the user
  • Extracts the response message using regular expressions from the <response></response> element ChatRelay.cshtml embeds in the rendered web page.

This pattern repeats until the user is finished chatting with the chatbot. At this point, we have created a complete chatbot system with ChatScript at its core running on an Ubuntu Linux VM, a proxy or relay page to make the chatbot available via AJAX friendly HTTP requests, and a web page with a friendly interface for the user to interact with that facilitates the chat session. Now it's time to work on the chatbot itself.

A Simple Chatbot for Lost & Found Visitors

PREFACE: The chatbot described here is a basic chatbot. It does not indicate at all the amazing depth of features in ChatScript. It is designed solely to give you a taste of how easy it is to create a chatbot in ChatScript, especially for programmers like yourself. However, like peeling an onion, the great thing about ChatScript is when you do need greater flexibility in topic control, enhanced parsing capability, and programming language style features like macros and more, you will find those capabilities and more when you embark on more ambitious projects.

You can try out the chatbot described here by visiting the Help section for the Lost & Found web site. You will find the file that implements this chatbot in this article's download list in the file named ChatEssentials.zip.

Here is a sample interaction between a user and the simple chatbot I implemented for the Lost & Found web site. Note, I am displaying the discourse as a linear series of interactions instead of a bulky stream of screenshots using the actual GUI interface shown in Step 10, which only shows the latest user input and chatbot response. Also, the chatbot's internal name is Harry and I have arbitrarily given the visiting user the name CatLover17. As mentioned before, the user's IP address is currently being used as a guest ID:

PHP
HARRY:_ Hello, welcome to FindAPet. How can I help you?
CatLover17:_> How much does it cost to use the Lost & Found?
HARRY:_ There is no charge to use the Lost & Found system.
CatLover17:_> Do I need to have a credit card?
HARRY:_ No you do not. There is no charge to use the Lost & Found system.
CatLover17:_>

Here are the ChatScript rules that facilitated this exchange:

PHP
# These rules handle questions from the user regarding
#  the cost of using the system.
#
#!x*** COST OF THE SYSTEM

#! How much does it cost to use the system?
u: FREE (how much)
	There is no charge to use the Lost & Found system.
	
#! Is it free?
u: (is * free)
	^reuse(FREE)	

#! What is the cost to use the system?
u: (what * cost)
	^reuse(FREE)	
	
#! Do I need a credit card?
u: (credit card)
	No you do not.
	^reuse(FREE)

Comment lines in ChatScript begin with the hash "#" character. However, lines that start with a hash character followed by an exclamation point ("#!") have a very special meaning. They are rule test comments and the "#!" is the rule test prefix. You can use the ":verify" meta-command to ask ChatScript to verify the rules in your topic file. ChatScript will take the example sentence that follows the rule test prefix and apply it to the rule immediately following it, as if the user entered it during a session. If the rule fails to match against the example sentence, a warning will be printed out. This is very useful for unit testing your chatbot when you make a lot of new changes that might have broken older rules in the system!

TIP: ":verify [topic name]" can be used to verify a single topic. For example ":verify ~introductions".

Rule Structure

ChatScript rules have the following structure:

[rule type]: {rule label} (match pattern)
[body] 

[rule type]: The rule type is a single character from a set consisting of the 4 different rule types, and it is required:

  • "?" - the rule will only match (or "fire") if the user asked a question and the content of the question is matched by the match pattern.
  • "s" - the rule will only match if the user made a non-interrogatory response and the content of the statement is matched by the match pattern.
  • "u"- the union of "?" and "s". The rule will match if the match pattern matches the content of the last user input, regardless of the input type, so questions or a statements are considered for a match.
  • "t" - The topic gambit. This is a statement the chatbot will display to the user if the chatbot has control of the conversation, which happens when it is not currently reacting to a question or statement made by the user.

{rule label}: A rule label is a constant that exists solely to allow other rules to refer to it. It is optional, and you will see an example of one shortly. Rule labels are optional.

(match pattern): The match pattern is the expression that ChatScript will apply against the user's input to decide whether or not to execute the rule.

[body]: The body of the rule that contains the actions to take if the rule fires. These actions can include: setting internal variables, chaining to other rules, displaying text to the user, and more.

TIP: Rules execute linearly within a topic. Therefore, you must put rules that match more specific word patterns before other rules with more general word patterns that can also match the same user input. Otherwise, especially when you use the keep keyword with a rule, there's a good chance the more general rule will prevent the more specific rule from ever firing, since it will match first if it precedes the more specific rule in the topic file. (More on the keep keyword in a moment).

Pattern Matching

IMPORTANT: One of the powerful features built-in to ChatScript is its ability to canonicalize words in the user's input. This allows you to write concise rules that can handle many different sentence patterns with a minimum of effort because ChatScript is helping you to generalize your match pattern. For example, the canonical form of a verb is the infinitive tense. If you used the word "be" in a match pattern it would match "[is, was, are, etc.]" since they are all forms of the verb "be".

TIP: If you can remember, always use the canonical form of a word in a match pattern so it generalizes well. If you want to match only the exact form of a word of phrase, use double or single quotes. You will find more details on canonicalization and exact matches in the ChatScript Basic user manual.

Let's take one of the rules from the rules above that respond to user input concerning the cost of the Lost & Found system:

PHP
u: FREE (how much)
	There is no charge to use the Lost & Found system. 

The rule type is "u" indicating the rule will respond to any input from the user. The match pattern is simply the words "how" and "much". This rule will fire if the user's input contains the words "how much" anywhere in the sentence, as long as they appear in the same order as the match pattern. Therefore the following sentences and many others will trigger this rule:

  • How much does the system cost?
  • How much does it cost to use the system?
  • (Any other sentence that has "how much" in it in the same word order).

TIP: To match words against a user sentence no matter what order they appear in the sentence, read up on the "<< [words to match] >>" syntax.

If this rule triggers than the sentence "There is no charge to use the Lost & Found system" will be displayed to the user since that is the only action found in the rule's body.

Rule Labels and the Reuse Keyword

Here's the second rule that responds to input from the user regarding the cost of the system:

PHP
u: (is * free)
	^reuse(FREE)

ChatScript match pattern's can contain wildcard characters that are used in a manner similar to that found in regular expressions. It will consume 0 or more words in a sentence during the matching process. In the match pattern (is * free), this means that pattern will match any sentence that has "is" in it and "free" in it, even if there are 1 or more words in-between them. Therefore, this rule will match the following sentences:

  • Is it free?
  • Is the system free?
  • Is the system always free for users?
  • (any sentence with "is" in it followed by "free", with 0 or more words between them)

Wildcards widen the number of sentences a match pattern can match, increasing its flexibility, but at the potential cost of matching unwanted sentences too and increasing the likelihood that the rule will interfere with other rules in the topic. The tip above about specific vs. general rules applies here too.

TIP: ChatScript has several useful wildcard variants. Consult the Basic user manual for more information.

ADVANCED TIP: You can even craft match patterns based on the part-of-speech associated with words. For example, you could have a match pattern that only matched the noun "run" and would not match the verb "run". Details can be found in the ChatScript documentation.

Take a quick look again at the rules list above for the rules that handle user questions about the cost of the system. Notice that all the rules following the first rule have the same body that contains the single action to reuse the rule labeled FREE. The reuse keyword eliminates the need to duplicate rule bodies between rules that perform the same basic task in response to user input that means roughly the same thing. The task in this context is to display the message "There is no charge to use the Lost & Found system" to the user when they ask about the system's cost. Without the reuse keyword, we would have to duplicate that action across all the rules. However, each rule is free to take its own actions as well as you can see in the credit card rule:

PHP
u: (credit card)
	No you do not.
	^reuse(FREE)

That rule first displays the message "No you do not." to the user before passing control to the first rule. The net output to the user then becomes: "No you do not. There is no charge to use the Lost & Found system". This gives you tremendous flexibility in crafting succinct rules that can handle many different situations, with a minimum of unnecessary duplication between the contents of each rule.

TIP: The reuse keyword and its brethren must be preceded by the hat character ("^") when they appear in a rule. That's how ChatScript knows it's not a common word.

Rejoinders. Rules with Context.

Frequently, you have questions or responses that are only relevant in the context of a previous question or response. ChatScript has rules that are in a class known as rejoinders. Take a look at the rules below. Note that the rules below belong to the ~questions topic, as indicated by the first line, not the ~introductions topic. The repeat, keep, and nostay keywords will be discussed in a minute:

PHP
topic: ~questions repeat keep nostay []

# Do we know if the user has a photo?
u: (!$hascatphoto)
	Do you have a photo of your cat?
	
	# Rejoinder looking for "yes" response or equivalent.
	a: (~yes)
		Good. When we are done talking, please upload it to the Lost & Found.
	
		# Remember that they have a photo.
		$hascatphoto = true
		
		# Ask the next relevant question.
		^respond(~questions)
		
	# Rejoinder looking for "no" response or equivalent.
	a: (~no)
		OK.  
		
		# Remember that they don't have a photo.
		$hascatphoto = false
		
		# Ask the next relevant question.
		^respond(~questions)

Variables and Word Sets

This gives me a chance to talk about variables and word sets now. Variable names in ChatScript are prefixed with a dollar sign ("$b"). ChatScript also supports the negation operator symbolized by the exclamation point "!". Therefore, the match pattern of the top level rule here matches if the $hascatphoto variable is undefined, indicating we have not gotten an answer yet from the user as to whether or not they have a photo of their lost cat. If this rule fires, it immediately displays the message "Do you have a photo of your cat?" and waits for the user to respond.

This is where rejoinders come into play. All rejoinder rules must start with a single letter in the range of "a through q", taking the place of one of the 4 rule types discussed earlier. This letter indicates the rejoinder nesting depth. Rejoinders can be nested and if so, the letter should advance to the next letter in the alphabet at each depth (i.e. - "a:", "b:", "c:", etc.). Since our example only goes one level down, both rejoinders have "a:" for the rejoinder nesting depth character. The example shows two rejoinder rules, one for handling a yes response from the user and the other one for handling a no response.

Notice that the words no and yes are preceded by a tilde ("~") character. In the case of (~yes) This tells ChatScript to match any response that equates to a positive response (yes, sure, OK, etc.) In the case of (~no) This tells ChatScript to match any response that equates to a negative response (no, nope, etc.). When a word is preceded by a tilde character, it indicates the word is actually the name of a concept set. Concept sets are lists of words that represent the same semantic meaning, at least as far as a rule is concerned, and can be reused throughout your chatbot to eliminate the duplication of common word lists when crafting topic rules.

Notice that the different value each rejoinder assigns to the $hasphoto variable. You will see later how this affects the chatbot's interaction with the user.

The Respond keyword

You can also see that each rejoinder ends with a respond statement. Since the word ^respond is preceded by a tilde, this indicates it's a ChatScript function call. The statement ^respond(~questions) tells ChatScript to make a call to the ~questions topic. However, as mentioned previously, the rejoinder rules shown here are members of the ~questions topic so this works out to a recursive call into the topic. This is done to make sure that every question that needs to be answered by the user gets a chance to execute. To solidify our knowledge, here is a brief description of another rule in the ~questions topic. Take a look at the rule that asks the user for the cat's breed:

PHP
#!x*** CAT BREED

# TIP: This rule must follow the other cat breed rules becaue it is more general than
#  they are and will fire before they do.		
#
u: (!$hasbreed)
	What breed is your missing cat?  If more than one, just list them.		

	# My cat's breed is.
	a: ASKCATBREED (is _*)
		$catbreed = '_0
	
	# Just accept the user's last input as the entire answer to the breed question.	
	a: (_*)
		^reuse(ASKCATBREED)

The rule's match pattern indicates it will fire if we don't have a valid value for the variable $hasbreed yet. This rule introduces you to the subject of capture variables, another concept familiar to those who work with regular expressions. If you look at the match patterns for the rejoinders used by this rule, you'll see the word "is" followed by an underscore prefixing a wildcard ("_*"). This tells ChatScript to capture all of the user's input that follows the word "is". For example, if the user said "My cat is a tabby cat" ChatScript would assign the words "is a tabby cat" to the $catbreed variable. Capture variables are accessed in the body of a rule by using capture variable index labels. These index labels are numbered from 0 upwards and correspond positionally to the order in which content was captured from the user's input. Since we only have one capture group, to access the content of that group and assign its value to the $catbreed variable we use "_0" as the capture variable index label. In this case, the value of the capture group is the text "is a tabby cat". The reason for prefixing the capture variable index label with an apostrophe ("'") is to keep ChatScript from trying to interpret or modify the value of the capture group before handing it over to the $catbreed variable. You will find lots of interesting information about ChatScript's built-in text processing features in the Basic and Advanced user manuals. However, in this case, we want exactly what the user typed without modification, since it's a cat breed and not a common word or phrase.

Putting It All Together

I am reproducing the rules for the entire questions gauntlet below so I can describe the overall pattern of interaction to you, to make sure you get the global picture of what is happening here:

PHP
#!x*** QUESTIONS GAUNTLET

# This topic keeps asking questions until the user provides the basic
#  information needed to help find their cat.

topic: ~questions repeat keep nostay []

#!x*** USER PHOTO OF CAT

# Do we know if the user has a photo?
u: (!$hascatphoto)
	Do you have a photo of your cat?
	
	# Rejoinder looking for "yes" response or equivalent.
	a: (~yes)
		Good. When we are done talking, please upload it to the Lost & Found.
	
		# Remember that they have a photo.
		$hascatphoto = true
		
		# Ask the next relevant question.
		^respond(~questions)
		
	# Rejoinder looking for "no" response or equivalent.
	a: (~no)
		OK.  
		
		# Remember that they don't have a photo.
		$hascatphoto = false
		
		# Ask the next relevant question.
		^respond(~questions)
		
#!x*** CAT'S NAME

# TIP: This rule must follow the other cat name rules becaue it is more general than
#  they are and will fire before they do.		
#
# Do we know the cat's name?
u: (!$catname)
	# Set a variable so we know the last question we asked was the cat's name.
	$askcatname = true
	What is the name of your cat?
	
	#! My cat's name is Malfie
	a: ASKCATNAME (is _*)
		# Save the cat's name. We prepend an apostrophe so that
		#  ChatScript does not attempt to interpret the cat's name
		#  and will give it to us unaltered as given by the user.
		$catname = '_0
		
		# Ask the next relevant question.
		^respond(~questions)
		
	#! I call her Trixie.
	a: (call* * _*)
		^reuse(ASKCATNAME)
		
	# This pattern is the catch-all in case the above patterns don't match since
	#  the user just said the cat's name as a response.
	#! Trixie.	
	a: (_*)
		^reuse(ASKCATNAME)

#!x*** CAT BREED

# TIP: This rule must follow the other cat breed rules becaue it is more general than
#  they are and will fire before they do.		
#
u: (!$hasbreed)
	What breed is your missing cat?  If more than one, just list them.		

	# My cat's breed is.
	a: ASKCATBREED (is _*)
		$catbreed = '_0
	
	# Just accept the user's last input as the entire answer to the breed question.	
	a: (_*)
		^reuse(ASKCATBREED)

As you can see, the questions gauntlet consists of three rules that each get one piece of information from the user about their lost cat. In order they are: Photo Yes/No, Name, and Breed. With each acquisition, the input received is assigned to the correct variable. In order, they are: $hasphoto, $catname, and $catbreed.

But how do the parts all fit together? Once the question gauntlet has control, it keeps calling itself recursively using the ^respond(~questions) statement covered earlier, that each rule except the last rule finishes with. The reason that the last rule doesn't have a call to ^respond is because by the time it is executed and gets the answer it is looking for, all the questions have been answered and the ~questions topic is finished. If we added more questions after the CAT BREED rule in the topic, we would have to add a trailing ^respond call to it too.

The last piece of the puzzle is to display to the user the knowledge we have accumulated about their cat. Take a look at these two rules that belong to the ~introductions topic, not the ~questions topic:

PHP
# If we have successfully collected the basic information
#  we need from the user, summarize it and display it back
#  to the user with the proper adjustment made for whether
#  or not the user has a photo of their lost cat.  Otherwise,
#  we keep asking questions until we do.
#
#!x*** SUMMARIZE USER INFORMATION AND DISPLAY

# User has provided all the required information and has a photo.				
u: ($catname $catbreed $hascatphoto=true)
	Ok.  You said your cat's name is $catname
	and that the breed was $catbreed and that
	you have a photo.  You are now ready to
	use the Lost & Found system. 

# User has provided all the required information and does not have
#  has photo.				
u: ($catname $catbreed $hascatphoto=false)
	Ok.  You said your cat's name is $catname
	and that the breed was $catbreed and that
	you do not have a photo.  You are now ready to
	use the Lost & Found system.  However, if you
	find a photo of your lost cat, please return here
	and upload it to the system.

# User has not provided all the required information yet.
#  
u: (!$catname)
	^respond(questions)
u: (!$catbreed)
	^respond(questions)
u: (!$hasphoto)
	^respond(questions)

Look at the first rule that has the match pattern of ($catname $catbreed $hascatphoto=true). This rule will fire when $catname and $catbreed have valid values, and $hascatphoto has the value of true. In other words, this rule will fire when all 3 pieces of information have been acquired from the user and the user indicated that they have a photo of their lost cat. It will read back the values the user gave and tell them they are ready to use the Lost & Found system.

The second rule is almost identical except it will fire instead of the first rule if the user indicated that they did not have a photo of their lost cat. In that case, the values acquired will again be read back to the user with the suggestion that they return and upload a photo of their cat, if they later find one.

The last three rules shown in the last above are there to trigger the question gauntlet if any of the required variables still do not have a valid value yet. Each one checks to see if a specific variable does not have a valid value yet and if so, control is transferred to the questions gauntlet.

That is how the different parts of the rules in our topic file interact and how control is transferred between topics. On that note, here's a brief discussion of the keep, repeat, and nostay topic keywords.

Avoiding Repetition and Topic Flow Control

The chatbot demonstrated here that provides help to Lost & Found visitors is a simple chatbot. We are not trying to tell a story, but merely trying to answer a few common questions the user may have and obtain 3 pieces of information necessary to search nearby humane shelters for the user's cat. Therefore, within reason, we don't care if we repeat ourselves and we are not looking to create the illusion of being a real character in a story. More elaborate storytelling chatbots work very hard to avoid repetition, something that did not concern us. ChatScript has several features to enforce variety in the output from a chatbot. These features are covered in detail in the ChatScript documentation. However, in the interest of being thorough, here is a terse discussion of the repetition and topic flow control keywords you saw in this article:

  • repeat - Normally, a rule is not allowed to produce duplicate output to the user. That is, it is not allowed to repeat itself. Adding this keyword allows a rule to repeat itself, making it easier to craft a lightweight chatbot.
  • keep - The default setting for the ChatScript engine is to "discard" a rule after it has been used. This makes sure that all or most rules in the chatbot get a chance to execute, creating additional variety. The keep keyword defeats this mechanism and allows a rule to be reused endlessly.
  • nostay - Normally, ChatScript will stay inside of a topic until it is exhausted. In our chatbot's case, we want it to exit the ~questions topic as soon as a piece of information was obtained from the user. The reason we still get all 3 pieces of information once we enter the topic is due to the recursive call we make to the ~questions topic with the help of the ^respond(~questions) statement. Once all 3 recursive calls exit back to the top, control is immediately transferred back to the ~introductions topic. If it weren't for the nostay keyword, we would stay in the ~questions topic instead, an unwanted behavior.

TIP: keywords like those above are not prefixed by the hat sign when they appear in a ~topic declaration, as they must be when found inside a rule body. ChatScript knows they are not common words because they appear in a topic declaration.

TIP: You can use sets of words/phrases to add variety to your chatbot. For example, in the CAT BREED question, we display the question "What breed is your missing cat?". If we instead used "What breed is your [missing lost awol] cat?", then each time ChatScript asked the question, it would choose randomly a different word from the words that are a synonym for "lost". For example: "What breed is your missing cat?", or "What breed is your lost cat?", etc. Check the ChatScript documentation for more details on this feature.

The Net Result - A Full Chat Session Log

Here is an actual chat log that shows a full session with a user. As you read it, see if you can map your new knowledge on how the rules work to what is happening between the user and the chatbot:

PHP
HARRY:_ Hello, welcome to FindAPet. How can I help you?
CatLover17:_> How much does it cost to use the Lost & Found?
HARRY:_ There is no charge to use the Lost & Found system.
CatLover17:_> Do I need a credit card?
HARRY:_ No you do not. There is no charge to use the Lost & Found system.
CatLover17:_> What do I do to use it?
HARRY:_ Do you have a photo of your cat?
CatLover17:_> Yes I do.
HARRY:_ Good. When we are done talking, please upload it to the Lost & Found. 
What is the name of your cat?
CatLover17:_> Her name is Roxy.
HARRY:_ What breed is your missing cat? If more than one, just list them.
CatLover17:_> Roxy is an Oriental Short-Hair.
HARRY:_ Ok. You said your cat's name is Roxy and that the breed was 
an Oriental Short hair and that you have a photo. 
You are now ready to use the Lost & Found system.
CatLover17:_>

A Final Note About ChatScript

A chatbot is only as sophisticated and clever as you make it. Fortunately with ChatScript, you can make it as sophisticated and clever as you want due to its extensive capabilities of which only a tiny fraction was covered in this article. Yet even this simple chatbot adds real value to the Lost & Found web site, saving the user from wading through help pages or FAQs while answering key questions they have about how the site works.

ChatBot Development TIP 1: Read the Basic Overview manual found in the ChatScript DOCUMENTATION sub-directory, followed by the Tutorial. They are both short, easy reads and will give you the basic knowledge you need to design and implement your chatbot.

ChatBot Development TIP 2: Do not try to anticipate every question or response a user will make of your chatbot. You can easily end up with a lot of contradictory rules that will make supporting and debugging your logic painful. The fun and fruitful way to create a truly mature and capable chatbot is to start with one that covers the most basic user input and to evolve it. The development cycle you will enter consists of analyzing your chat logs each day, discovering what questions users are asking or what responses they are giving that are not being handled properly. Then add new rules and facts to support those questions while occasionally running the ":verify" command to make sure you haven't broken anything. (See the Advanced user manual for information on the ":verify" command.) You will find that ChatScript has solid tools to analyze your chat logs in the Analytics manual.

Conclusion

Azure makes provisioning and configuring a virtual machine easy and a virtual machine is the gateway to adding high power value to your site by providing killer web services that go above and beyond the capabilities of a common web site or web service. Azure is equally adept at supporting non-Windows operating systems so if your favorite tool only runs under, or runs best under Linux or some other exotic operating environment, there is nothing stopping you from implementing it. Finally, a wonderful concrete example of a scenario that requires a virtual machine is a chatbot. With ChatScript, you can add an entertaining and informative virtual assistant easily to your web site, hosted by Azure, and powered by a Linux virtual machine. You can talk to the Lost & Found ChatBot using the link below:

Talk to the Lost & Found ChatBot

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)