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

Comments


March 21. 2010 07:19
trackback
ButtonChrome.com | A C# AIML Chatterbot – Artificial Intelligence In 500 Lines Of Code

Thank you for submitting this cool story - Trackback from DotNetShoutout


March 21. 2010 22:53
trackback
ButtonChrome.com | A C# AIML Chatterbot ? Artificial Intelligence In 500 Lines Of Code

Thank you for submitting this cool story - Trackback from iAwaaz-News-by-People


Israel Lior 
March 22. 2010 02:13
Lior
For 4 hours this is brilliant. Does it support <standalone/> tags?

What other languages do you know?


March 22. 2010 08:38
Dean Chalk
I dont think it supports all available tags (ran out of time). I certainly doesnt support <standalone/> - although it would be easy to implement.
I miight look at writing an implementation in F# (micrsofots new .NET language), which actually would be quite interesting beause there is a lot of functional programming paradigms that miught work well for this context


March 24. 2010 01:15
trackback
Social comments and analytics for this post

This post was mentioned on Twitter by ButtonChrome: Just wrote an artificially intelligent 'chatterbot' in 500 lines of C# - http://tinyurl.com/yetsge5 . Start chatting to your laptop now !


Israel Lior 
March 26. 2010 11:39
Lior
>you could extend this application and include a ‘learning mode’.
But AIML's specs don't have anything like that. The "learn" command just reads already made AIML files.


April 28. 2010 00:41
affordable web design
Thats awesome!  I remember when I encountered a chat bot for the first time. It blew my mind!


May 27. 2010 07:40
pingback
Pingback from topsy.com

Twitter Trackbacks for
        
        Dean Chalk's Blog | A C# AIML Chatterbot – Artificial Intelligence In 500 Lines Of Code
        [deanchalk.me.uk]
        on Topsy.com


United States Financial magazine 
April 24. 2011 15:01
Financial magazine
trackback
financial-magazine.net | A C# AIML Chatterbot ? Artificial Intelligence In 500 Lines Of Code

Thank you for submitting this cool story - Trackback from financial-magazine.net

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading

FAO Comment Spammers : Please note that this blog is fully moderated by me personally and no comment spam will ever appear on this site.



RecentComments

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

© Copyright 2012 Dean Chalk's Blog