A C# AIML Chatterbot – Artificial Intelligence In 500 Lines Of Code

by Dean 20. March 2010 21:28

I recently stumbled across an area of artificial intelligence programming called AIML ‘chatterbots’. These programs are interpreters for an XML based AI language called AIML. AIML and the code that processes it are the basis of the first and most famous chatterbot called A.L.I.C.E, where the founders and followers are infamous for promoting and winning the Loebner Prize in Artificial Intelligence.

Among the available technologies, there are the usual suspects including implementations in PHP, Perl, Python, Java and even Pascal – but nothing in .NET

On Saturday my wife (who runs her own gourmet coffee roasting business here in rural Kent – www.coffeebeanshop.co.uk) was in Germany buying a new coffee roaster, so to fill time I decided to have a go at a C# implementation, which is detailed below.

Firstly, before you do anything, you need to download some AIML files, which describe how the interpreter should handle human conversation, and sets out the rules of engagement. AIML also includes it’s own XML based mini-language, which was tough to implement. To get enough AIML to make this worthwhile, I recommend that you get files from this location - http://www.alicebot.org/aiml/alice.zip . This is the AIML created by Richard Wallace – winner of the Loebner Prize on at least one occasion.

Once you have the AIML, just use the code below, and you can chat to your own Artificial Intelligence ‘Bot’ for as long as you like :)

 

Firstly, the console program to run this thing

class Program
{
    static void Main(string[] args)
    {
        var ai = new AIMLProcessor();
        ai.Load();
        var chat = Console.ReadLine();
        AIMLProcessor.Thats.Add("*");
        while (chat != "")
        {
            AIMLProcessor.Inputs.Insert(0, chat);
            AIMLProcessor.Thats.Insert(0, ai.FindTemplate(chat, AIMLProcessor.Thats[0], "*"));
            Console.WriteLine(AIMLProcessor.Thats[0]);
            chat = Console.ReadLine();
        }
    }
}

Then you need the classes to do the work

public class AIMLProcessor
{
    public static readonly List<string> Thats = new List<string>();
    public static readonly List<string> Inputs = new List<string>();
    private readonly Dictionary<string, string> BotData = new Dictionary<string, string>();
    private readonly Dictionary<string, string> Predicates = new Dictionary<string, string>();
    private readonly AIMLData data = new AIMLData(null, string.Empty);
    private readonly Random rand = new Random(DateTime.Now.Millisecond);
 
    public void Load()
    {
        // download from http://www.alicebot.org/aiml/alice.zip and unzip locally
        // then use that location for loading, as below
        foreach (string file in Directory.GetFiles(
            @"D:\Users\Administrator\Downloads\alice", "*.aiml"))
        {
            var doc = XDocument.Load(file);
            var first = doc.Element("aiml");
            var topics = first.Elements("topic");
            var cats = first.Elements("category");
            if (topics != null && topics.Count() > 0)
            {
                foreach (var topic in topics)
                {
                    var topicName = topic.Attribute("name") == null ?
                        "*" : topic.Attribute("name").Value;
                    foreach (var cat in topic.Elements())
                        ProcessCategory(cat, topicName);
                }
            }
            if (cats == null || cats.Count() <= 0) 
                continue;
            foreach (var cat in cats)
                ProcessCategory(cat, "*");
        }
 
        LoadInitialPredicates();
    }
 
    private void ProcessCategory(XElement cat, string topic)
    {
        var patt = cat.Element("pattern");
        var temp = cat.Element("template");
        var that = cat.Element("that");
        if (patt == null || temp == null)
            return;
        var t = that != null ? that.InnerText() : "*";
        string pattern = string.Format("{0} <THAT> {1} <TOPIC> {2}", 
            patt.Nodes().First(), t.Trim(), topic.Trim());
        LoadWord(data, new List<string>(pattern.Split(' ')), temp.InnerText());
    }
 
    public string FindTemplate(string text, string that, string topic)
    {
        var pattern = string.Format("{0} <THAT> {1} <TOPIC> {2}", 
            Normalise(text).Trim(), "*", topic.Trim());
        var temp = FindTemplateRecursive(data, new List<string>(pattern.Split(' ')), 
            new PatternData(), 0);
        var result = InterpretTemplate(temp.Template, temp.WildTexts, text, that, topic);
        return result.Trim().Replace("  ", " ");
    }
 
    public string Normalise(string text)
    {
        return Regex.Replace(text, @"[^\w\ ]", "").Trim().Replace("  ", " ");
    }
 
    private string InterpretTemplate(string template, List<WildData> wilds, 
        string text, string that, string topic)
    {
        var xmltemplate = string.Format("<process>{0}</process>", template);
        var doc = XElement.Parse(xmltemplate);
        if (doc.Elements().Count() == 0)
            return doc.Value;
        string response = "";
 
        foreach (var node in doc.Nodes())
        {
            if (node.NodeType != XmlNodeType.Element)
            {
                response += " " + node;
                continue;
            }
            var element = node as XElement;
            switch (element.Name.LocalName.ToUpper())
            {
                case "INPUT":
                    response += " " + ProcessInput(element).Trim();
                    break;
                case "THAT":
                    response += " " + ProcessThat(element).Trim();
                    break;
                case "THATSTAR":
                    response += " " + ProcessThatStar(element, wilds).Trim();
                    break;
                case "TOPICSTAR":
                    response += " " + ProcessTopicStar(element, wilds).Trim();
                    break;
                case "STAR":
                    response += " " + ProcessStar(element, wilds).Trim();
                    break;
                case "PERSON":
                    response += " " + ProcessPerson(wilds).Trim();
                    break;
                case "SET":
                    response += " " + ProcessSet(element, wilds, text, that, topic).Trim();
                    break;
                case "GET":
                    response += " " + ProcessGet(element).Trim();
                    break;
                case "THINK":
                    response += " " + ProcessThink(element, wilds, text, that, topic).Trim();
                    break;
                case "RANDOM":
                    response += " " + ProcessRandom(element, wilds, text, that, topic).Trim();
                    break;
                case "BOT":
                    response += " " + ProcessBot(element).Trim();
                    break;
                case "SR":
                    response += " " + ProcessSR(element, wilds, text, that, topic).Trim();
                    break;
                case "SRAI":
                    response += " " + ProcessSRAI(element, wilds, text, that, topic).Trim();
                    break;
                default:
                    return element.ToString().Trim();
            }
        }
        return response.Trim();
    }
 
    private static string ProcessInput(XElement element)
    {
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= Inputs.Count)
            return string.Empty;
        return Inputs[num];
    }
 
    private static string ProcessThat(XElement element)
    {
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= Thats.Count)
            return string.Empty;
        return Thats[num];
    }
 
    private static string ProcessThatStar(XElement element, IEnumerable<WildData> wilds)
    {
        var w = wilds.Where(a => a.WildType == StarType.That).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= w.Count)
            return string.Empty;
        return w[num].WildText;
    }
 
    private static string ProcessTopicStar(XElement element, IEnumerable<WildData> wilds)
    {
        var w = wilds.Where(a => a.WildType == StarType.Topic).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value.Split(',')[0]) - 1;
        if (num >= w.Count)
            return string.Empty;
        return w[num].WildText;
    }
 
    private static string ProcessStar(XElement element, IEnumerable<WildData> wilds)
    {
        var w = wilds.Where(a => a.WildType == StarType.Pattern).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value) - 1;
        if (num >= w.Count)
            return string.Empty;
        return w[num].WildText;
    }
 
    private static string ProcessPerson(IList<WildData> wilds)
    {
        if (wilds.Count == 0)
            return string.Empty;
        var words = wilds[0].WildText.Split(' ');
        for (int i = 0; i < words.Count(); i++)
        {
            if (words[i].Trim().ToUpper() == "I")
                words[i] = "you";
            if (words[i].Trim().ToUpper() == "MY")
                words[i] = "your";
        }
        var response = string.Join(" ", words);
        response = response.Replace("you am", "you are");
        return response;
    }
 
    private string ProcessSet(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        if (element.Attribute("name") == null)
            return string.Empty;
        var att = element.Attribute("name").Value;
        if (!Predicates.ContainsKey(att))
            Predicates.Add(att, "");
        Predicates[att] = InterpretTemplate(element.InnerText(), wilds, text, that, topic);
        return Predicates[att];
    }
 
    private string ProcessGet(XElement element)
    {
        if (element.Attribute("name") == null)
            return string.Empty;
        var att = element.Attribute("name").Value;
        if (!Predicates.ContainsKey(att))
            return string.Empty;
        return Predicates[att];
    }
 
 
    private string ProcessThink(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        InterpretTemplate(element.InnerText(), wilds, text, that, topic);
        return string.Empty;
    }
 
    private string ProcessRandom(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        var num = rand.Next(0, element.Elements().Count());
        var ret = element.Elements().ToList()[num].InnerText();
        return InterpretTemplate(ret, wilds, text, that, topic);
    }
 
    private string ProcessSRAI(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        var t = InterpretTemplate(element.InnerText(), wilds, text, that, topic);
        return FindTemplate(t, that, topic);
    }
 
    private string ProcessSR(XElement element, List<WildData> wilds, string text, 
        string that, string topic)
    {
        var w = wilds.Where(a => a.WildType == StarType.Pattern).ToList();
        if (w.Count() == 0)
            return string.Empty;
        var num = 0;
        var index = element.Attribute("index");
        if (index != null)
            num = int.Parse(index.Value) - 1;
        if (num >= w.Count)
            return string.Empty;
        var t = InterpretTemplate(w[num].WildText, wilds, text, that, topic);
        return FindTemplate(t, that, topic);
    }
 
    private string ProcessBot(XElement element)
    {
        return BotData[element.Attribute("name").Value.ToUpper()];
    }
 
 
    private static PatternData FindTemplateRecursive(AIMLData ai, List<string> text, 
        PatternData data, int searchPos)
    {
        var key = text[searchPos];
        if (data.IsInWildcard && searchPos < text.Count - 1 && !ai.Data.ContainsKey("_") &&
            !ai.Data.ContainsKey(key.ToUpper()) && !ai.Data.ContainsKey("*"))
        {
            data.WildTexts[data.WildTexts.Count - 1].WildText += key + " ";
            return FindTemplateRecursive(ai, text, data, searchPos + 1);
        }
        if (ai.Data.ContainsKey("_"))
        {
            if (searchPos == text.Count - 1)
            {
                data.IsAnswer = true;
                data.Template = ai.Data["_"].Template;
                return data;
            }
            data.WildTexts.Add(new WildData {WildText = key + " ", WildType = data.WildType});
            data.IsInWildcard = true;
            data = FindTemplateRecursive(ai.Data["_"], text, data, searchPos + 1);
            if (data.IsAnswer)
                return data;
            data.WildTexts.RemoveAt(data.WildTexts.Count - 1);
            data.WildType = data.WildTexts.Count == 0 ? StarType.Pattern : 
                data.WildTexts[data.WildTexts.Count - 1].WildType;
        }
        if (ai.Data.ContainsKey(key.ToUpper()))
        {
            if (searchPos == text.Count - 1)
            {
                data.IsAnswer = true;
                data.Template = ai.Data[key.ToUpper()].Template;
                return data;
            }
            if (key.ToUpper() == "<THAT>")
                data.WildType = StarType.That;
            if (key.ToUpper() == "<TOPIC>")
                data.WildType = StarType.Topic;
            data.IsInWildcard = false;
            data = FindTemplateRecursive(ai.Data[key.ToUpper()], text, data, searchPos + 1);
            if (data.IsAnswer)
                return data;
        }
        if (!data.IsAnswer && ai.Data.ContainsKey("*"))
        {
            if (searchPos == text.Count - 1)
            {
                data.IsAnswer = true;
                data.Template = ai.Data["*"].Template;
                return data;
            }
            data.WildTexts.Add(new WildData {WildText = key + " ", WildType = data.WildType});
            data.IsInWildcard = true;
            data = FindTemplateRecursive(ai.Data["*"], text, data, searchPos + 1);
            if (data.IsAnswer)
                return data;
            data.WildTexts.RemoveAt(data.WildTexts.Count - 1);
            data.WildType = data.WildTexts.Count == 0 ? StarType.Pattern : 
                data.WildTexts[data.WildTexts.Count - 1].WildType;
        }
        return data;
    }
 
    private static void LoadWord(AIMLData parent, List<string> pattern, string template)
    {
        var key = pattern[0].ToUpper().Trim();
        pattern.RemoveAt(0);
        if (!parent.Data.ContainsKey(key))
            parent.Data.Add(key, new AIMLData(parent, key));
        if (pattern.Count > 0)
            LoadWord(parent.Data[key], pattern, template);
        else
            parent.Data[key].Template = template;
    }
 
    private void LoadInitialPredicates()
    {
        BotData.Add("NAME", "Gunther");
        BotData.Add("RELIGION", "Buddha Bubba is my mesiah");
        BotData.Add("PARTY", "monster raving loony party");
        BotData.Add("GENDER", "male");
        BotData.Add("SIGN", "Capricorn");
        BotData.Add("ARCH", "Windows 7");
        BotData.Add("BIRTHDAY", "6/6/1666");
        BotData.Add("SPECIES", "chatterbot");
        BotData.Add("GENUS", "computer algorithm");
        BotData.Add("FOAVOURITEFOOD", "chicken");
        BotData.Add("BOTMASTER", "programmer");
        BotData.Add("MASTER", "The Evil Genius");
        BotData.Add("AGE", "32");
        BotData.Add("FRIEND", "Robocop");
        BotData.Add("LOCATION", "Inside a VAIO");
        BotData.Add("FAMILY", "intelligence");
        BotData.Add("KINGDOM", "In the UK");
        BotData.Add("ORDER", "program");
        BotData.Add("PHYLUM", "AI");
        BotData.Add("FORFUN", "I like fiddling with my switches");
        BotData.Add("FRIENDS", "Chico, Harpo, Groucho and Karl");
    }
}
 
public class AIMLData
{
    public AIMLData(AIMLData parent, string key)
    {
        Parent = parent;
        Key = key;
        Data = new Dictionary<string, AIMLData>();
    }
 
    public Dictionary<string, AIMLData> Data { get; private set; }
    public AIMLData Parent { get; private set; }
    public string Template { get; set; }
    public string Key { get; set; }
}
 
public class PatternData
{
    public PatternData()
    {
        WildTexts = new List<WildData>();
        WildType = StarType.Pattern;
    }
 
    public string Template { get; set; }
    public bool IsAnswer { get; set; }
    public bool IsInWildcard { get; set; }
    public StarType WildType { get; set; }
    public List<WildData> WildTexts { get; private set; }
}
 
public class WildData
{
    public string WildText { get; set; }
    public StarType WildType { get; set; }
}
 
public enum StarType
{
    Pattern,
    That,
    Topic
}
 
public static class ExtensionMethods
{
    public static string InnerText(this XElement element)
    {
        return element.Nodes().Aggregate("", 
            (current, node) => current + node.ToString());
    }
}

And there it is – its all you need.

I don’t doubt there are many refinements that could included (this WAS done in about 4 hours), and extra features that may be interesting.

Here is a screenshot of one of our first conversations:

aimlbot

As you can see, Gunther (the unfortunate name I gave it – did I mention my wife was in Germany ?), is quite a conversationalist. Not all conversations were quite as convincing, but you could extend this application and include a ‘learning mode’ where over time you can increase the Bots ability to say the right thing at the right time :)

If anyone has any suggestions or comments, the please email me (see contact details) or add a comment on this blog post

Happy Chatterbotting :)

 

Worthwhile Links:

http://alicebot.blogspot.com/
http://www.alicebot.org/aiml.html
http://www.loebner.net/Prizef/loebner-prize.html
http://en.wikipedia.org/wiki/AIML

Tags:

C# | AIML

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010 Dean Chalk's Blog