Invasion of the Robots!

By Matt Duffin.

In July’s issue of the newsletter, we introduced you to the Invasion of the Robots Contest and its British counterpart, Team Britbot. These competitions invite you to create a conversational robot, or ‘bot’, that can be used with MSN® Messenger and Windows® Live™ Messenger to be in for a chance to share some fantastic prizes. These robots, which chat with you over Messenger in the same way as any other contact, can range from personal assistants or joke tellers through to coursework researchers or TV guide helpers; they are literally limited only by your imagination!

With the competitions closing soon (Team Britbot on 1 September 2006 and Robot Invaders on 15 September 2006), I’ve produced a quick guide to creating a simple ‘my first bot’ that can be extended easily into your actual competition entry.

The bot will have a simple existence. When asked by a user to remember something, it will do so under a particular name, keeping remembered facts separate between users. When needed to recall something, the user just needs to tell the bot what name the fact was remembered under. Hardly groundbreaking, but a simple foundation upon which you can build something extraordinary.

To create the bot, you’ll require Visual Studio®.NET 2005 (Express Editions are fine) and moderate knowledge of how to use it. The tutorial will focus on C#, but the code samples can be easily converted into Visual Basic®.NET or your favourite .NET language. You’ll also need a bot SDK (Software Development Kit). These consist of libraries and tools that create a wrapper around the Messenger service, saving you the trouble of worrying about messaging protocols and instead allowing you to focus on the functionality of your bot. Robot Invaders has teamed up with three SDK providers – Akonix, Colloquis and Incesoft. Each SDK is different: Akonix comes in a quite heavy download and uses a web-based administration system, Colloquis provides powerful natural language processing at the cost of increased complexity, and Incesoft supplies a small yet simple SDK.

For this tutorial, we’ll be using the Incesoft SDK. However, when you move out of experimentation and begin to put your bot into production, you may wish to consider one of the other SDKs. This is because Incesoft does not provide ‘provisioning’ – bot hosting. This means that for your bot to work, the computer it runs on must be permanently switched on and connected to the Internet. However, its small size and easy to learn interface make it a good choice when getting started.

First, you’ll need to create a new MSN Hotmail, MSN Messenger, or Microsoft Passport account; this will be the address that the bot uses on Messenger. Once you’ve done this, sign up for the Incesoft SDK at https://sp.incesoft.com/. They’ll send you a confirmation message with details for downloading the SDK. Once you’ve received your username and password, login on the Incesoft website. From the menu on the left-hand side, choose ‘MSN account management’.

Getting Started

Choose ‘Add new account’ and enter the address and password for your bot. Once this is done, choose ‘Sign In’ to connect the bot to the Messenger service. If you wish, you can edit other bot settings from this page, including display name, picture, etc. Once you’ve finished, you can now add the bot to your Messenger contact list. Right now, you’ll see that it is ‘Away’ and that any attempts to talk to it will result in the message ‘service provider offline’. This is because Incesoft acts as a proxy between the Messenger service and the computer running the bot – marshalling Messenger commands into their SDK equivalent and vice-versa. At the moment, your computer hasn’t connected to the Incesoft service, so the generic ‘service provided offline’ message is returned instead.

After downloading the SDK, unzip the contents to a suitable directory. You’ll notice that there’s a sample project included with the SDK; this sample is extremely useful for learning about the SDK’s features. However, for this tutorial, we’re going to create a project from scratch. Start Visual Studio and create a new Windows Forms project called ‘Robot’. Copy ‘BotPlatformSDK.dll’ from the ‘Lib’ directory of the SDK to the ‘bin’ directory of your project and add a reference to it within the project; this DLL is the wrapper described earlier.

During this project, controls are left with their default names for easy referral. You’re encouraged to name them sensibly, though! Also, best practises and exception handling are left by the wayside so that the amount of code you have to copy is lower.

Add a StatusStrip onto the form and add a StatusLabel to that, ensuring that its Text property is empty. Add a SplitContainer onto the form and set its Orientation to Horizontal. Size the top panel so that it takes up about ¼ of the available height. Set the FixedPanel property of the SplitContainer to Panel1 (or whatever the top panel is called) to ensure that it maintains its size whereas the bottom panel resizes to fill the available space. Add another SplitContainer into the bottom panel and add a DataGridView into each of these, ensuring that Adding, Editing and Deleting is disabled and that they are both docked. Also set AutoSizeColumnsMode to Fill on both of these: this will ensure that the columns resize automatically with the window.

Add other controls as necessary so that your form design matches that below:

Giving your Bot a Memory

Next, create two XML files in the same directory as your executable (usually ‘bin/Debug’). ‘memory.xml’ will be used to store the actual facts requested, whereas ‘log.xml’ will store debugging information that allows you to follow the bot’s activities.

Add the following test content to ‘log.xml’:

<?xml version="1.0" encoding="UTF-8"?>

<log>

  <event source="EventTestFactory" message="This is a test..." />

</log>

And the following (or something similar if you’re not too fond of squashed bananas) to ‘memory.xml’:

<?xml version="1.0" encoding="utf-8" ?>

<memories>

  <memory key="birthday" value="09/05/1986" user="testuser@contoso.com"/>

  <memory key="recipe" value="Take two squashed bananas and cover liberally in sugar. Bake for 20 minutes and eat." user="testuser@contoso.com"/>

  <memory key="birthday" value="15/02/1956" user="anothertest@contoso.com"/>

  <memory key="wife" value="Sandra" user="anothertest@contoso.com"/>

</memories>

Now, enter the following code within the definition of your form class:

        public static string logFile = System.IO.Path.Combine(Application.StartupPath, "log.xml");

        public static string memoryFile = System.IO.Path.Combine(Application.StartupPath, "memory.xml");

        public Form1()

        {

            InitializeComponent();

            this.Load += new EventHandler(Form1_Load);

        }

        private void bindXMLData(string xmlFile, string dataMember, DataGridView dataGridView)

        {

            DataSet ds = new DataSet();

            ds.ReadXml(xmlFile);

            BindingSource bs = new BindingSource();

            bs.DataSource = ds;

            bs.DataMember = dataMember;

            dataGridView.DataSource = bs;

        }

        private void updateLog()

        {

            bindXMLData(logFile, "event", this.dataGridView1);

        }

        private void updateMemory()

        {

            bindXMLData(memoryFile, "memory", this.dataGridView2);

        }

        public void updateXML()

        {

            updateLog();

            updateMemory();

        }

        private void Form1_Load(object sender, EventArgs e)

        {

            updateXML();

        }

Now, when you run your application, the DataGridViews will be bound to the two XML files that you have just created:

Now that you have the ability to display the contents of the log file, you need to be able to write log entries. Add a new class to your project called ‘Logger’ and enter the following code for it:

using System;

using System.Xml;

namespace Robot

{

    public class Logger

    {

        public Logger () { }

        public static void Log(string source, string message)

        {

            try

            {

                XmlDocument logFile = new XmlDocument();

                logFile.Load(Form1.logFile);

                XmlElement Event = logFile.CreateElement("event");

                XmlAttribute Source = logFile.CreateAttribute("source");

                Source.Value = source;

                Event.Attributes.Append(Source);

                XmlAttribute Message = logFile.CreateAttribute("message");

                Message.Value = message;

      Event.Attributes.Append(Message);

                logFile.GetElementsByTagName("log").Item(0).AppendChild(Event);

                logFile.Save(Form1.logFile);

            }

            catch (Exception)

            {

                return;

            }

        }

    }

}

 

Now, calling the ‘Log’ method with a message and message source will cause a log entry to be written to ‘log.xml’. Next, you’ll need to add memory functionality to the project. Users will need to be able to add memories under a given key and recall those memories. For simplicity, the bot will not check whether a specific memory exists before overwriting it nor provide the ability to forget memories, but this may be functionality that you wish to add. The bot must be able to cope with many users, therefore the add and recall methods will need to take a user parameter.

Add a new class to your project called ‘Memory’ and enter the following code for it:

using System;

using System.Xml;

namespace Robot

{

  class Memory

    {

        public Memory() { }

        public static void Remember(string user, string key, string value)

        {

            try

            {

                XmlDocument memoryFile = new XmlDocument();

                memoryFile.Load(Form1.memoryFile);

                XmlElement Memory = memoryFile.CreateElement("memory");

                XmlAttribute Key = memoryFile.CreateAttribute("key");

                Key.Value = key;

                Memory.Attributes.Append(Key);

                XmlAttribute Value = memoryFile.CreateAttribute("value");

                Value.Value = value;

                Memory.Attributes.Append(Value);

                XmlAttribute User = memoryFile.CreateAttribute("user");

                User.Value = user;

                Memory.Attributes.Append(User);

                memoryFile.GetElementsByTagName("memories").Item(0).AppendChild(Memory);

                memoryFile.Save(Form1.memoryFile);

            }

            catch (Exception)

            {

                return;

            }

        }

        public static string Recall(string user, string key)

        {

            try

            {

                XmlDocument memoryFile = new XmlDocument();

                memoryFile.Load(Form1.memoryFile);

                /* Selects nodes from the memory file according to an XPath expression that reads:

                 * Get all 'memory' nodes from the root node 'memories' whose 'user' attribute is equal

                 * to the 'user' parameter. */

                XmlNodeList userMemories = memoryFile.SelectNodes("/memories/memory[@user='" + user + "']");

                foreach (XmlNode memory in userMemories)

                {

                    if (memory.Attributes["key"].Value.ToLower() == key.ToLower())

                        return memory.Attributes["value"].Value;

                }

      return null;

            }

            catch (Exception)

            {

                return null;

            }

        }

    }

}

Now, calling the ‘Remember’ method with a memory, key and user will cause a memory to be written to ‘memory.xml’. Calling ‘Recall’ with a key and user will cause that memory to be read from ‘memory.xml’ if it exists; ‘null’ will be returned if not.

Connecting to the Service

Next, add the functionality to connect and disconnect your bot server to Incesoft’s server. Add the following ‘using’ statements at the top of the form code:

using Incesoft.BotPlatform.SDK;

using Incesoft.BotPlatform.SDK.Interface;

Add the following variables to the form:

private string address = "msnbot.incesoft.com";

private Int16 port = 6602;

private IRobotServer server;

These contain the address of Incesoft’s server and the port to connect to. A bot server is also declared. Next, amend the form’s constructor so that it reads as follows:

public Form1()

        {

            InitializeComponent();

            this.Load += new EventHandler(Form1_Load);

            this.button1.Click += new EventHandler(button1_Click);

            this.button2.Click += new EventHandler(button2_Click);

            this.toolStripStatusLabel1.Text = "Server Offline";

            server = RobotServerFactory.Instance.createRobotServer(address, port);

        }

This adds event handlers for the buttons and sets the status bar text to indicate that the server is currently offline. The bot server is also instantiated by a factory. Add the following event handlers to the form’s code:

        void button1_Click(object sender, EventArgs e)

        {

            this.server.login(this.textBox1.Text, this.textBox2.Text);

        }

        void button2_Click(object sender, EventArgs e)

        {

            this.server.logout();

        }

This causes the bot server to log on or off when the buttons are clicked. Notice that when the buttons are clicked the online or offline text on the status bar is not changed, you’ll find out why in a minute! Also, you’ll notice that, while the bot’s status is now ‘Online’, attempts to communicate with it either time out, or result in “service provider offline”.

IRobotConnectionListener and IRobotHandler

The SDK provides two interfaces, ‘IRobotConnectionListener’ and ‘IRobotHandler’, which, when implemented, allow you to respond to bot events. Create classes in the ‘Robot’ namespace that implement these interfaces, similar to the following:

public class RobotConnectionListener : IRobotConnectionListener

    {

        public void serverConnected(IRobotServer server)

        {

        }

        public void serverDisconnected(IRobotServer server)

        {

        }

        public void serverLoggedIn(IRobotServer server)

       {

        }

        public void serverReconnected(IRobotServer server)

        {

        }

    }

    public class RobotHandler : IRobotHandler

    {

        public void activityAccepted(IRobotSession session)

        {

        }

        public void activityRejected(IRobotSession session)

        {

        }

        public void exceptionCaught(IRobotSession session, Exception cause)

        {

        }

        public void messageReceived(IRobotSession session, IRobotMessage message)

        {

        }

        public void nudgeReceived(IRobotSession session)

        {

        }

        public void sessionClosed(IRobotSession session)

        {

        }

        public void sessionOpened(IRobotSession session, int OpenMode)

        {

        }

    }

‘IRobotConnectionListener’ contains definitions for connection-related events, such as the server being connected, disconnected, logging on and reconnecting (by default, the server attempts to reconnect automatically when disconnected). ‘IRobotHandler’, however, contains definitions for Messenger events, such as when a message is received, a session is opened, etc.

Next, make moderate adjustments to the connection listener class. First, add a constructor that takes a ‘Form1’ argument and stores it locally. This will allow access to the status bar on the form, allowing the connection status of the server to be reported, as well as the ‘updateXML’ method. Your code should look something like the following:

private Form1 containingForm;

        public RobotConnectionListener(Form1 containingForm)

        {

            this.containingForm = containingForm;

        }

Do the same for the robot handler class. Add the following lines to the bottom of the form’s constructor to associate instances of your new connection listener and robot handler classes with the server:

            server.addConnectionListener(new RobotConnectionListener(this));

            server.addRobotHandler(new RobotHandler(this));

Reporting Server State

Now you’ll want to update the status bar label reporting the current online status of your robot. ‘serverConnected’, ‘serverDisconnected’ and ‘serverReconnected’ should set the status bar label to “Server Offline”. “Server Online” should be set only when ‘serverLoggedIn’ occurs. While updating the status of the label, you may wish to add log entries corresponding to the events. Also, when the server automatically reconnects, you’ll need to have it log in again.

The code to do all this will be similar to the following:

(In Form1)

        public void UpdateStatusLabel(bool status)

        {

            this.toolStripStatusLabel1.Text = "Server " + (status ? "Online" : "Offline");

        }

(In ConnectionListener)

        public void serverConnected(IRobotServer server)

        {

            Logger.Log(this.GetType().ToString(), "Server connected...");

            containingForm.UpdateStatusLabel(false);

        }

        public void serverDisconnected(IRobotServer server)

        {

            Logger.Log(this.GetType().ToString(), "Server disconnected...");

            containingForm.UpdateStatusLabel(false);

        }

        public void serverLoggedIn(IRobotServer server)

        {

            Logger.Log(this.GetType().ToString(), "Server logged in...");

            containingForm.UpdateStatusLabel(true);

        }

        public void serverReconnected(IRobotServer server)

        {

            Logger.Log(this.GetType().ToString(), "Server reconnected...");

            containingForm.UpdateStatusLabel(false);

            /* Log back in to the server */

            server.login(

                containingForm.Controls.Find("TextBox1", true)[0].Text,

                containingForm.Controls.Find("TextBox2", true)[0].Text);

        }

Still following? Great! You’ll find that even though you’ve added log entries, the XML displays do not update. However, having your form updated with the current XML data is somewhat more of a challenge than it may first seem. After adding a log entry within the ConnectionListener, it is not possible to update the DataGridViews directly, nor is it possible to call a public method within the form that causes an update. This is because the ConnectionListener operates on a separate thread; attempting to update the DataGridViews from a thread other than the main UI thread causes an exception.

To get around this, add a delegate to the declaration section of the form, as follows:

        public delegate void ConnectionListenerDelegate();

        public ConnectionListenerDelegate updateXMLDelegate;

Now, in the form’s constructor, add the following:

        updateXMLDelegate = this.updateXML;

This ties the delegate to the ‘updateXML’ method. Next, invoke the delegate whenever the XML data needs to be updated, as follows:

(e.g.)

        public void serverConnected(IRobotServer server)

        {

            Logger.Log(this.GetType().ToString(), "Server connected...");

            containingForm.UpdateStatusLabel(false);

            containingForm.BeginInvoke(containingForm.updateXMLDelegate);

        }

Now, the display of the XML data will be updated after each new entry.

Into the Heart of the Robot

Now that connecting and disconnecting are taken care of, you can actually get to grips with the main functionality of the robot. In the ‘sessionOpened’ method of the ‘RobotHandler’, add the following:

            session.send("Hello, welcome to RememberBot!");

            Logger.Log(this.GetType().ToString(), "Session opened " + session.getUser().ID);

            containingForm.BeginInvoke(containingForm.updateXMLDelegate);

The addition of one line of code results in the following when you first talk to your bot:

You’ll also see that a log entry has been added detailing the user who’s talked to your bot.

The only other method that you’re going to program against during this tutorial is the ‘messageReceived’ event. However, the SDK provides the ability to react to nudges (perhaps by sending your own back!), send and respond to activities (see the SDK documentation for more details) and deal with exceptions.

Enter the following code for the ‘messageReceived’ method:

        public void messageReceived(IRobotSession session, IRobotMessage message)

        {

            Logger.Log(this.GetType().ToString(), "Message received " + session.getUser().ID);

           

            string[] splitMsg = message.getString().Split(new string[1] { " " }, StringSplitOptions.None);

       if (splitMsg[0].ToLower().Equals("remember"))

            {

                string key = splitMsg[1];

                string value = message.getString().Replace("remember " + key + " ", String.Empty);

                Memory.Remember(session.getUser().ID, key, value);

                session.send("I remembered \"" + value + "\" as \"" + key + "\".");

            }

            else if (splitMsg[0].ToLower().Equals("recall"))

            {

                string memory = Memory.Recall(session.getUser().ID, splitMsg[1]);

                if (memory != null)

                {

                    session.send(memory);

                }

                else

                {

                    session.send("Sorry " + session.getUser().FriendlyName + ", I don't remember \"" + splitMsg[1] + "\".");

                }

            }

            else

            {

                session.send("I don't understand what you want me to do.");

            }

            containingForm.BeginInvoke(containingForm.updateXMLDelegate);

        }

At first glance, that might seem quite complex (at least it did to me!), so let’s go through it step by step:

1. A log entry indicating that a message was received is added. The user’s ID (email address) is added to the entry,

2. An array containing each word of the original message is created using Split,

3. If the first word of the message is ‘remember’:

a. The key of the memory is the second word of the message,

b. ‘remember’ plus the key are removed from the message to give the value,

c. ‘Remember’ is called with the user’s email address to save the memory,

d. The user is told what was remembered.

4. If the first word of the message is ‘recall’:

a. ‘Recall’ is called with the user’s email address and the key (second word of the message),

b. If the memory is found, the user is told what was recalled,

c. If not, the user is informed that the memory could not be found.

5. If the first word of the message is anything else, the user is told that the robot does not understand.

So, to save “Eggs, Beans, Bacon” under “shopping list”, the user would enter:

Remember shoppinglist Eggs, Beans, Bacon

And would enter:

Recall shoppinglist

To retrieve it. The following is an example session with the bot, and the corresponding form display:

One thing that you may notice is that if you send a greeting to the bot as your first message (such as “Hi Robot!”), you’ll receive “Hello, welcome to RememberBot!” and will then be told "I don't understand what you want me to do." To get around this, you can set a Boolean flag indicating whether this is the first message from the user and bypass sending ‘I don’t understand’ for the first message. However, because the bot could potentially deal with hundreds of users, you’ll need to keep a flag for each one. This could be accomplished using a private member variable (‘Dictionary<string, bool>’), which is added to when the session is opened and removed from when the session is closed. Adding this functionality is left as an exercise for you!

Conclusion

Phew, so that’s the bot! It’s hardly the most functional of creations, and it’s unlikely to win any prizes, but hopefully it’s served as a springboard for your more interesting designs. Best of luck in creating your bot, and of course in the competitions.

If you need any assistance creating your bot, each SDK provider has an excellent set of forums where you can chat to other ‘botficionados’. You can also email me on ukstuzin@microsoft.com.

Until next time, Matt.