Using SpeakToMe – A First Project

December 3, 2012 at 2:51 PMAdministrator

 

As I announced some time ago, I have released my natural language processor to Codeplex.  The documentation on the site is fairly complete, but I wanted to take a different approach on my blog by going at it in more a tutorial fashion starting from downloading the binaries and integrating them into your project.  From there, I will be occasionally posting some tips and tricks that show how flexible the engine is and how to get the most out of it.  Let’s have a look at how to get going with the engine.

Obtaining the Binaries

The first step in building a new project with SpeakToMe is to obtain the binaries containing the types we’ll need.  To get them, go to the Codeplex site’s download page and download the binaries release (SpeakToMe_bin_2012_10_19 as of this writing).  Once the zip file is downloaded, extract the files to a place where you can find them, later.  The zip file should contain three assemblies: Microsoft.Practices.Prism.dll, SpeakToMe.Core.dll and SpeakToMe.Speech.dll.

Create a New Project

Start Visual Studio and create a new WPF project.  The new project dialog should look similar to the below.

Sample Project

Click OK to generate the project.

We need to set reference to the SpeakToMe assemblies.  To do that, create a sub-folder in your new project (I called mine ThirdParty) and copy the three assemblies, mentioned above, into the folder.  Next, in the project references, add references to the three assemblies.

Supporting Users and Conversations

There are a couple of pieces of plumbing we need to build before we get into the fun stuff.  One of these is a data layer that knows how to access information about users and conversations.  Why is this necessary?  SpeakToMe has built in support for multiple users and multiple ways for users to access the engine.  If multiple conversations are going on, the engine needs to be able to figure out how to get a response back to the correct user.  If one user is accessing the system via email and another via chat, there must be a way to insure the correct channel is used to send the response.  So, for the purposes tracking this information, some form of data storage must be implemented.

SpeakToMe does not dictate how you do this.  Instead, it provides an interface describing the needed functionality which you must implement.  There are also types the engine expects to work with which are defined for you.  Let’s see how to pull this all together by building the necessary parts.

Creating a Data Store

The example code that is included in the SpeakToMe source download includes an embedded database implementation.  We’ll use the same schema to create a new database on a local instance of SQL Express.

I have created a new database named SpeakToMe and have created three tables in that database named “Users”, “Conversations” and “ConversationHistory”.  The schema looks like this.

SpeakToMe_Schema

Let’s take a moment to discuss the data that will be stored here and what each column holds.  First of all, each table has an integer key column that is auto-incrementing.  Now, starting with the Users table, the column are pretty self-explanatory where the user name is for display in tooling and the first and last name can be used anywhere.  Next, look at the conversation table.  Each entry in this table describes a single conversation with a single user.  The initiated and active columns describe when the conversations started and whether or not it is still active.  In some cases we are able to determine whether the user has ended the conversation and in other situations we cannot.  If a user ends an IM session with us, we can assume the conversation is over.  If we get an email from a user, we can’t determine whether or not we’ll receive another, so conversations for this type of communication tend to just stay open.

NOTE: I won’t be discussing building functionality for communicating over email and IM in this article, but stay tuned!

The Mode and Address columns are used to determine what channel the communication came from.  Mode is defined as an enumeration in the Core assembly, so we know how we got the message.  However, what if a single user initiates two distinct conversations from two different email accounts?  We include address, here, to make the distinction between the two.  Lastly, UserId denotes the user we’re talking to.

The ConversationHistory table contains each individual communication between the user and the system.  Message and MessageDateTime contain the text of the message and when it was sent.  UserInitiated is true if the message came from the user and is false if it originated with the system.  Tag and TagType are used to store a piece of state along with a particular message so it can be retrieved and referenced later on in the conversation.

Be sure to add the foreign keys between the tables so you’ll be able to access table references on your entities.  I find that creating a Database Diagram is an easy way to do this.

dbdiag

 

Creating a Data Access project.

For this example, I’m going to be using the Entity Framework to access data so I need to create a class library project that will contain my data access code and create a conceptual model.

Add a new class library project to the solution you started.  I’m calling mine “SpeakToMe.Sample.Data”.  Next, delete the class1.cs file that is generated for you.

Now, we need to add the Entity Framework to our data project.  We’ll use NuGet to do this by right-clicking on the references note in the project and selecting “Manage NuGet packages…”.  When the dialog appears, enter “entity framework” into the search box.  The list should populate.  Select the entry in the list entitled “EntityFramework” and click the install button.

addef

After the libraries have been installed into your project, close the NuGet dialog.  Now we need to create our model from the database.  Right-click your project and select Add->New Item.  Select “ADO.Net Entity Data Model” and specify a name for your model.  I named mine “SpeakToMe”.  Click the “Add” button.

AddModel

In the wizard, select “Generate from database” and click next.  Now, click the Create New Connection button and select your server (local for me) and the database you created.

CreateConnection

Click the “Test Connection” button to ensure the information is correct.  If so, click OK.  As a convenience, you might want to leave the Save Connection String checkbox checked.  This will create the connectionstring entry in your config file for use later.

save_connection

Click Next and wait for the database information to populate in the next screen.  Now, drill down to the tables you created and select all of them.

TableList

Click “Finish” and your model will be created.

Implementing the IUserData Interface

We need to add a class, now, that implements the IUserData interface.  In your data project, add a class named UserData and add the attributes shown below.

UserData Class
  1. namespace SpeakToMe.Sample.Data
  2. {
  3.     [Export(typeof(IUserData))]
  4.     [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.Shared)]
  5.     public class UserData : IUserData
  6.     {
  7.         public Core.Models.ConversationHistory AddConversationHistory(int conversationId, string text, string tagString, string tagType, bool userInitiated)
  8.         {
  9.             throw new NotImplementedException();
  10.         }
  11.  
  12.         public Core.Models.Conversation CreateConversation(int userId, Core.Enums.ConversationType type, string address)
  13.         {
  14.             throw new NotImplementedException();
  15.         }
  16.  
  17.         public Core.Models.User CreateUser(string userName, string firstName, string lastName)
  18.         {
  19.             throw new NotImplementedException();
  20.         }
  21.  
  22.         public Core.Models.Conversation GetConversation(int userId, Core.Enums.ConversationType type, string address)
  23.         {
  24.             throw new NotImplementedException();
  25.         }
  26.  
  27.         public List<Core.Models.ConversationHistory> GetConversationHistory(int conversationId)
  28.         {
  29.             throw new NotImplementedException();
  30.         }
  31.  
  32.         public Core.Models.User GetUserById(int id)
  33.         {
  34.             throw new NotImplementedException();
  35.         }
  36.     }
  37. }

Note that you’ll need to set references to SpeakToMe.Core and System.ComponentModel.Composition in order to be able to successfully compile this class.  As for the attributes on the class, these allow the use of Microsoft Extensibility Framework (MEF).  SpeakToMe uses MEF as a IoC container and the engine will use it to obtain an instance of this class when it needs to.

Now, we need to implement the methods on the interface.  Below is my implementation.

Completed UserData Class
  1. using SpeakToMe.Core.Interfaces;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.ComponentModel.Composition;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8.  
  9. namespace SpeakToMe.Sample.Data
  10. {
  11.     [Export(typeof(IUserData))]
  12.     [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.Shared)]
  13.     public class UserData : IUserData
  14.     {
  15.         public Core.Models.ConversationHistory AddConversationHistory(int conversationId, string text, string tagString, string tagType, bool userInitiated)
  16.         {
  17.             using (var ctx = new SpeakToMeEntities())
  18.             {
  19.                 //create new database entry using entity defined by EF
  20.                 SpeakToMe.Sample.Data.ConversationHistory ch = new ConversationHistory
  21.                 {
  22.                     ConversationId = conversationId,
  23.                     Message = text,
  24.                     MessageDateTime = DateTime.Now,
  25.                     Tag = tagString,
  26.                     TagType = tagType,
  27.                     UserInitiated = userInitiated
  28.                 };
  29.  
  30.                 ctx.ConversationHistories.Add(ch);
  31.                 ctx.SaveChanges();
  32.  
  33.                 //return an instance of the entity.  Note that this is not the entity defined by EF, but the one defined in SpeakToMe.Core which is the only one the engine would know about.
  34.  
  35.                 SpeakToMe.Core.Models.ConversationHistory conversationHistory = new Core.Models.ConversationHistory
  36.                 {
  37.                     ID = ch.ConversationId,
  38.                     Message = ch.Message,
  39.                     MessageDateTime = ch.MessageDateTime,
  40.                     Tag = ch.Tag,
  41.                     TagType = ch.TagType,
  42.                     UserInitiated = ch.UserInitiated
  43.                 };
  44.  
  45.                 return conversationHistory;
  46.  
  47.             }
  48.  
  49.         }
  50.  
  51.         public Core.Models.Conversation CreateConversation(int userId, Core.Enums.ConversationType type, string address)
  52.         {
  53.             using (var ctx = new SpeakToMeEntities())
  54.             {
  55.                 Conversation conv = new Conversation
  56.                 {
  57.                     Active = true, //must be true if we're creating a conversation
  58.                     Address = address,
  59.                     Initiated = DateTime.Now,
  60.                     Mode = (int)type,
  61.                     UserId = userId
  62.                 };
  63.  
  64.                 ctx.Conversations.Add(conv);
  65.                 ctx.SaveChanges();
  66.  
  67.                 Core.Models.Conversation conversation = new Core.Models.Conversation
  68.                 {
  69.                     Active = conv.Active,
  70.                     Address = conv.Address,
  71.                     ID = conv.ConversationId,
  72.                     Initiated = conv.Initiated,
  73.                     Mode = (Core.Enums.ConversationType) conv.Mode,
  74.                     UserId = conv.UserId
  75.                 };
  76.  
  77.                 return conversation;
  78.             }
  79.         }
  80.  
  81.         public Core.Models.User CreateUser(string userName, string firstName, string lastName)
  82.         {
  83.             using (var ctx = new SpeakToMeEntities())
  84.             {
  85.                 User usr = new User
  86.                 {
  87.                     FirstName = firstName,
  88.                     LastName = lastName,
  89.                     UserName = userName
  90.                 };
  91.  
  92.                 ctx.Users.Add(usr);
  93.                 ctx.SaveChanges();
  94.  
  95.                 Core.Models.User user = new Core.Models.User
  96.                 {
  97.                     FirstName = usr.FirstName,
  98.                     LastName = usr.LastName,
  99.                     ID = usr.UserId,
  100.                     UserName = usr.UserName
  101.                 };
  102.  
  103.                 return user;
  104.             }
  105.         }
  106.  
  107.         public Core.Models.Conversation GetConversation(int userId, Core.Enums.ConversationType type, string address)
  108.         {
  109.             using (var ctx = new SpeakToMeEntities())
  110.             {
  111.                 Core.Models.Conversation conversation = ctx.Conversations.Where(c => c.UserId == userId && c.Mode == (int)type && c.Address == address).Select(c => new Core.Models.Conversation
  112.                     {
  113.                         Active = c.Active,
  114.                         Address = c.Address,
  115.                         ID = c.ConversationId,
  116.                         Initiated = c.Initiated,
  117.                         Mode = (Core.Enums.ConversationType)c.Mode,
  118.                         UserId = c.UserId
  119.                     }).FirstOrDefault();
  120.  
  121.                 return conversation;
  122.  
  123.             }
  124.         }
  125.  
  126.         public List<Core.Models.ConversationHistory> GetConversationHistory(int conversationId)
  127.         {
  128.             List<Core.Models.ConversationHistory> messages = new List<Core.Models.ConversationHistory>();
  129.             using (var ctx = new SpeakToMeEntities())
  130.             {
  131.                 var msgs = ctx.ConversationHistories.Where(c => c.ConversationId == conversationId);
  132.  
  133.                 msgs.ToList().ForEach(m =>
  134.                     {
  135.                         messages.Add(new Core.Models.ConversationHistory
  136.                             {
  137.                                 ID = m.ConversationHistoryId,
  138.                                 Message = m.Message,
  139.                                 MessageDateTime = m.MessageDateTime,
  140.                                 Tag = m.Tag,
  141.                                 TagType = m.TagType,
  142.                                 UserInitiated = m.UserInitiated
  143.                             });
  144.                     });
  145.  
  146.                 return messages;
  147.             }
  148.         }
  149.  
  150.         public Core.Models.User GetUserById(int id)
  151.         {
  152.             using (var ctx = new SpeakToMeEntities())
  153.             {
  154.                 var usr = ctx.Users.Where(u => u.UserId == id).FirstOrDefault();
  155.  
  156.                 if (usr == null)
  157.                     return null;
  158.  
  159.                 return new Core.Models.User
  160.                 {
  161.                     FirstName = usr.FirstName,
  162.                     LastName = usr.LastName,
  163.                     ID = usr.UserId,
  164.                     UserName = usr.UserName
  165.                 };
  166.             }
  167.         }
  168.     }
  169. }

 

Presence

With SpeakToMe, a particular presence is a mode of communication.  In this post, I have already mentioned email and IM.  You could also implement SMS or any other communication protocol you desire.  In this sample, we’re going to create a simple test presence that will serve as the channel between our WPF application and SpeakToMe.  In a later post, I’ll describe how to build an XMPP (IM) presence.

Let’s create another new class library project to hold our presence class.  I’m calling mine SpeakToMe.Sample.Presence.  Delete the Class1.cs file that is created by default and create a new class called TestPresence.  Now add a references to SpeakToMe.Core and System.ComponentModel.Composition and make your class declaration look like this.

Code Snippet
  1. [Export(typeof(IPresence))]
  2. [PartCreationPolicy(CreationPolicy.Shared)]
  3. public class TestPresence : IPresence
  4. {
  5.     public void Initialize()
  6.     {
  7.         throw new NotImplementedException();
  8.     }
  9.  
  10.     public bool IsConnected
  11.     {
  12.         get { throw new NotImplementedException(); }
  13.     }
  14.  
  15.     public void ProcessCommand(string command, int userId, ISmartHomeServiceCallback callback)
  16.     {
  17.         throw new NotImplementedException();
  18.     }
  19.  
  20.     public void OnImportsSatisfied()
  21.     {
  22.         throw new NotImplementedException();
  23.     }

A couple of things to note, here.  First of all, you can tell from the class attributes that SpeakToMe is going to use MEF to load an instance of this class when it’s needed.  Also, this class is implementing another interface that is provided in SpeakToMe.Core.  Any class that will serve as another presence for the system will need to be exported this way and will also need to implement this same interface.  This approach allows you to add as many communication mechanisms as you like without having to touch the source code of the library. 

When implementing the interface, the Initialize method should be used to get everything set up for listening to a channel and sending messages through it.  For example, connecting to an email server or an XMPP server.  The IsConnected property should report whether or not the channel is open for communication.  If a connection is lost, for example, the property should return false.  All processing by the way of sending a message into the SpeakToMe engine should be handled in the ProcessCommand method.  This method has a void return type because we will be notified via an event when the response is ready to be sent back to the user.  Lastly, the OnImportsSatisfied will be called when MEF has supplied all the instances we have requested of it.  More on this in a second.  Now, set references to the SpeakToMe.Core, SpeakToMe.Speech and Microsoft.Practices.Prism assemblies we copied into the third party folder earlier.  Here is my implementation of the TestPresence class.

Completed TestPresence Class
  1. [Export(typeof(IPresence))]
  2.     [PartCreationPolicy(CreationPolicy.Shared)]
  3.     public class TestPresence : IPresence
  4.     {
  5.         [Import]
  6.         public IEventAggregator EventAggregator { get; set; }
  7.  
  8.         public bool IsConnected
  9.         {
  10.             get { return true; }
  11.         }
  12.  
  13.         public void Initialize()
  14.         {
  15.             //do nothing
  16.         }
  17.  
  18.         private void ReplyToChannelEventHandler(ReplyToChannelEventArgs args)
  19.         {
  20.             if (args.Mode == ConversationType.Test)
  21.             {
  22.                 //ConversationData.CreateConversationHistory(args.ConversationId, args.Reply, args.TagString, args.Tag, false);
  23.  
  24.                 if (args.Callback != null)
  25.                 {
  26.                     args.Callback.ReturnResult(args.Reply);
  27.                 }
  28.             }
  29.         }
  30.  
  31.         public void OnImportsSatisfied()
  32.         {
  33.             this.EventAggregator.GetEvent<ReplyToChannelEvent>().Subscribe(ReplyToChannelEventHandler);
  34.         }
  35.  
  36.         public void ProcessCommand(string command, int userId, ISmartHomeServiceCallback callback)
  37.         {
  38.             var processor = ServiceLocator.GetInstance<CommandProcessor>();
  39.             processor.ProcessCommand(command, userId, ConversationType.Test, "", callback);
  40.         }

Off the bat, you’ll notice the EventAggregator property and its associated [Import] attribute.  The attribute is one way we can ask MEF for an instance of a type.  If you look at the OnImportsSatisfied method, you’ll see we’re setting up an event listener there.  We want to do this setup here, because we are using an instance of the EventAggregator type and we want to make sure MEF has given it to us before we start using its reference.  For our sample, we’ll be calling ProcessCommand directly from our client and this method will pass the message on to the NLP engine for processing.  When processing is complete, we’ll be called back on the ReplyToChannelEventHandler where we call the passed in callback to send the reply back to the caller.  For other communication types, we might be sending an email or IM message instead of calling the callback.

The Bootstrapper

I mentioned, earlier, that SpeakToMe uses MEF to load types.  MEF provides an easy way to extend a system without having to put explicit code dependencies in place.  If you’re not familiar with MEF and how it works, I’d suggest you take a bit of time to look at the documentation here.

In a nutshell, however, MEF utilizes a catalog of types.  In this case, we need to point MEF at assemblies we want it to look at.  It will go through the types in the assembly and identify any that have an Export attribute on them.  These are then added to the catalog.  Now, when code needs an instance of the type, it can ask MEF to get one from the catalog.

There are many ways to specify to MEF which assemblies to include.  In our case, we’re going to do this in code.  SpeakToMe includes a BootStrapperBase class that automatically loads the assemblies it knows about.  But, hey, we just added those data access and presence projects and we need a way to tell MEF about them.  This is what the Bootstrapper class is for.  I should also note, however, that it is absolutely required that you implement the bootstrapper whether you have additional assemblies to load or not because without doing so, the default assemblies will never be loaded. 

In the WPF application you created at the beginning of this exercise, create a new class and call it Bootstrapper.  You’ll need to set a reference from that project to SpeakToMe.Speech.  Now, have the new class inherit from BootStrapperBase and override the AddCustomAssembliesToCatalog method

Bootstrapper
  1. public class Bootstrapper : BootStrapperBase
  2.     {
  3.         protected override void AddCustomAssembliesToCatalog(System.ComponentModel.Composition.Hosting.AggregateCatalog catalog)
  4.         {
  5.             base.AddCustomAssembliesToCatalog(catalog);
  6.         }
  7.     }

 

It is in this overridden method that we will add our custom assemblies like so.

Adding Assemblies To Catalog
  1. catalog.Catalogs.Add(new AssemblyCatalog(typeof(UserData).Assembly));
  2. catalog.Catalogs.Add(new AssemblyCatalog(typeof(TestPresence).Assembly));

You’ll need to add references to your projects in order to get a clean compile.

The client

Now that we’ve added all the required plumbing, we can start working on the client we’re going to use to communicate with the engine.  I’m going to start with the default MainWindow.xaml file that was created when we created our WPF project and add a ListBox, TextBox and Button to it like this.

window

The listbox is at the top a will contain our conversation.  The textbox is at the bottom and will be used to specify our input.  The button sends the input to the engine.  Now let’s name the controls and clean up a bit.  Here is the resulting Xaml.

Main Window Xaml
  1. <Window x:Class="SpeakToMe.Sample.MainWindow"
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.         Title="MainWindow" Height="350" Width="525">
  5.     <Grid>
  6.         <ListBox HorizontalAlignment="Left" Height="248" Margin="10,10,0,0" VerticalAlignment="Top" Width="497" x:Name="Conversation"/>
  7.         <TextBox HorizontalAlignment="Left" Height="34" Margin="25,275,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="397" x:Name="Message"/>
  8.         <Button Content="Say" HorizontalAlignment="Left" Margin="432,275,0,0" VerticalAlignment="Top" Width="75" Height="34" x:Name="SayButton"/>
  9.  
  10.     </Grid>
  11. </Window>

Let’s do some work in the code behind of this window, now.

First, let’s add some code to the constructor to create and initialize our MEF catalog using the bootstrapper class we created.

Code Snippet
  1. public MainWindow()
  2.       {
  3.           Bootstrapper bs = new Bootstrapper();
  4.           bs.Initialize();
  5.           InitializeComponent();
  6.       }

Now, we need to add an event handler to the button so we can do something when it’s clicked.

Here is the class after adding the event handler.

Code Snippet
  1. using SpeakToMe.Core;
  2. using SpeakToMe.Core.Interfaces;
  3. using SpeakToMe.Sample.Presence;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Windows;
  10. using System.Windows.Controls;
  11. using System.Windows.Data;
  12. using System.Windows.Documents;
  13. using System.Windows.Input;
  14. using System.Windows.Media;
  15. using System.Windows.Media.Imaging;
  16. using System.Windows.Navigation;
  17. using System.Windows.Shapes;
  18.  
  19. namespace SpeakToMe.Sample
  20. {
  21.     /// <summary>
  22.     /// Interaction logic for MainWindow.xaml
  23.     /// </summary>
  24.     public partial class MainWindow : Window
  25.     {
  26.         public MainWindow()
  27.         {
  28.             Bootstrapper bs = new Bootstrapper();
  29.             bs.Initialize();
  30.             InitializeComponent();
  31.         }
  32.  
  33.         private void SayButton_Click_1(object sender, RoutedEventArgs e)
  34.         {
  35.             this.Dispatcher.Invoke(new Action(() => this.Conversation.Items.Add(Message.Text)));
  36.             IPresence presence = ServiceLocator.GetInstance<TestPresence>();
  37.             presence.ProcessCommand(Message.Text, 1, new CallbackWrapper(new Action<string>((msg) =>
  38.             {
  39.                 this.Dispatcher.Invoke(new Action(() =>
  40.                 {
  41.                     Conversation.Items.Add(msg);
  42.                 }));
  43.             })));
  44.         }
  45.     }
  46. }

 

The first thing we do after the button is clicked is to add our message to the system to the listbox.  Next, we use the ServiceLocator to get an instance of our presence class.  The ServiceLocator is a class we can use to explicitly get an instance of a class from MEF.  If you’ll recall, we exported this class and it should be in our catalog.  Once we have an instance, we call ProcessCommand on that instance and pass in the message, a userId and a callback to be called when a response is ready.  All the callback does is add the system’s response to the listbox so we can observe it.

Initializing the Data in the Database

The last task we have to do before testing is to add some data to the database.  Use whatever tools you need to to add a single user to the users table.  You can specify whatever values you like for the columns.  Just make sure that the id you pass in the ProcessCommand call matches your user’s ID.

We also need an active conversation to be manually created for this test.  Create a new record in the Conversations table for this with the initiated value set to any datetime value, active set to true, mode = 0, address null and userId set to the id of the user you created.

One last thing.  When we were creating our entity model, a connection string was placed in the app.config file for the data project.  You’ll need to copy this connection string and paste it into the app.config file of your WPF app.

Testing

That completes the coding for the test.  Be sure to set the WPF application as the startup project and press F5.  The main window should open.  Type “hello” into the textbox and press the button.  After a second or two, you should see “Hello” followed by your user’s first name appear in the listbox.  This is the response from the system.  Congratulations!  You have just created an application that makes use of the SpeakToMe NLP engine.

Conclusion

Actually, this is more of a beginning than a conclusion.  I will be posting several posts pertaining to the SpeakToMe engine in the coming months.  I’ll cover authoring new token classes, building rules and creating an XMPP presence for the engine.  Along the way, I’ll be pointing out specific best practices and tips.  Hope to have you join me.

Once again, I’d like to let everyone know that I welcome and would greatly appreciate any community participation in this effort.  Contact me through comments on this blog or the SpeakToMe Codeplex site for details on getting started.

Posted in: Natural Language Processing | MEF | .Net | Database | SpeakToMe

Tags: , , , ,

My Natural Language Processing Engine is Now Open Source

October 20, 2012 at 1:16 PMAdministrator

After a lot of thought, I have decided to make my Natural Language Engine available as an open source project on Codeplex.  My reservations with doing so in the past had to do with my concern over the quality of the code and also the possibility of potentially monetizing the project at some point.  This processor was written alongside my smart home system and as such was both a bit sloppy as a prototype can be and also pretty tightly integrated with the smart home code.

My change of heart came when I realized that a stand-alone NLP engine is of little use to anyone, but once it becomes an interface to a more capable, feature rich system can be worth quite a bit.  To me, it seems better to put the NLP portion of my hobby project out there for the community to improve and innovate so we can all reap the rewards in the projects we build.  I’ve spent some time cleaning up the code and creating clean extension points.  Please have a look at http://SpeakToMe.Codeplex.com

Roadmap

  • First of all, I welcome anyone who would like to contribute to the project. Please contact me at ghostwrtrone at yahoo dot com.
  • My goal for the project is to both harden the existing project and also to innovate it by adding new features. Among the areas I’d like to see better implemented are the following.
  • · Better rule matching logic. Currently, exact matches are required on token types in the rule method signatures. I’d like a fuzzier matching algorithm so variation in input can still be matched to the proper rule.
  • · Expanded library of Tokens. There is no way to predict what domain the application will be used in and thus custom tokens will always be required. Adding more general tokens, however, would shorten the path to a completed application.
  • · Better implementation of questions. Currently, the library supports a system asking questions of the user to clarify their statements. This implementation basically places a question object in a collection along with an associated expected token list and a callback that is executed when the question is answered. This list specifies the tokens expected to be in the answer. When the answer is specified, the matching question is located, the callback is executed and the question is removed from the collection. This works ok in many situations, but there are flaws with it as well. First of all, if the answer to a question prompts the system to ask another question, the code structure of the nested callbacks is atrocious. Second, if a user has more than one question in the collection that is expecting the same token list in the answer, there is no way for the system to distinguish between the two questions.

I hope you enjoy the library. If you create a really cool project using it, please shoot me an email and tell me about it. Also, to those who wish to join in the effort, welcome. Let’s keep the project moving forward for all.

Posted in: .Net | Natural Language Processing

Tags: ,

Using AlchemyAPI to Classify Blog Posts–Source

August 24, 2012 at 10:28 AMAdministrator

Source Code: FeedEater.zip

Posted in: .Net | Natural Language Processing | TPL Dataflow

Tags: , ,

Using AlchemyAPI to Classify Blog Posts–Final

August 24, 2012 at 10:20 AMAdministrator

In part 1 and part 2 of this short series, we looked at AlchemyAPI and the classes they provide that simplify access to their services and we looked at an overview of the architecture of this application.  Today, I want to supply the source code for the app and discuss a bit about how it works.

Source Code

The Blocks

Looking at the class that is the heart of the application, FeedEaterApiGateway, you’ll see that I’m creating each of the Dataflow blocks using a factory such as below.

Code Snippet
  1. //block that downloads feed items from feed
  2.             var feedDownloadBlock = FeedDownloadBlockFactory.CreateFeedDownloadBlock();

Initially, I was creating all the blocks in the FeedEaterApiGateway class, itself, but the class soon became quite large and unwieldy so I used the factories as a way to place the creation of each block in it’s own file.  As each block is created, it is being places into a List<IDataflowBlock> collection so we can maintain a reference to them and prevent them from being garbage collected.

Chaining

At the bottom of the CreateBlocks method, I chain the blocks together into the pipeline that was discussed in an earlier post.

Chaining The Blocks
  1. feedDownloadBlock.LinkTo(processCategoryBlock, new DataflowLinkOptions { Append = true, MaxMessages = 10000, PropagateCompletion = true });
  2.           processCategoryBlock.LinkTo(processConceptsBlock, new DataflowLinkOptions { Append = true, MaxMessages = 10000, PropagateCompletion = true });
  3.           processConceptsBlock.LinkTo(processEntitiesBlock, new DataflowLinkOptions { Append = true, MaxMessages = 10000, PropagateCompletion = true });
  4.           processEntitiesBlock.LinkTo(processKeywordsBlock, new DataflowLinkOptions { Append = true, MaxMessages = 10000, PropagateCompletion = true });
  5.           processKeywordsBlock.LinkTo(processRelationsBlock, new DataflowLinkOptions { Append = true, MaxMessages = 10000, PropagateCompletion = true });
  6.           processRelationsBlock.LinkTo(processDbUpdatesBlock, new DataflowLinkOptions { Append = true, MaxMessages = 10000, PropagateCompletion = true });

Configuring the Blocks

Looking at the factory that creates the feedDownloadBlock, you’ll see that the delegate to execute is specified and then a list of parameters are specified to configure the block.

Block Configuration
  1. new ExecutionDataflowBlockOptions { BoundedCapacity = 1000, MaxDegreeOfParallelism = 1, MaxMessagesPerTask = 1 });

In particular, notice the MaxDegreeOfParallelism.  The value of 1 specifies that only one feed will be processed at a time.

Now, when we link this block to the next one in the pipeline (as in the previous code listing), MaxMessages specifies the size of the buffer between the two blocks and PropagateCompletion allows exception to be passed on through the chain (as well as a signal meaning that “this is the last message I’m sending you”).

The entire system is kicked off in the test ui.  In the code-behind for the MainWindow, you’ll find this.

Code Snippet
  1.  
  2. private void Button_Click_1(object sender, RoutedEventArgs e)
  3. {
  4.     FeedEaterApi.FeedEaterApiGateway fea = new FeedEaterApi.FeedEaterApiGateway();
  5.  
  6.     using (var ctx = new FeedsEntities())
  7.     {
  8.        foreach(var feed in ctx.Feeds)
  9.        {
  10.            fea.ProcessFeed(feed);
  11.        }
  12.     }
  13. }

Here, we fetch all the feeds from the database ad process each.  The ProcessFeed method looks like this.

Code Snippet
  1. public void ProcessFeed(Feed feed)
  2.         {
  3.             (_BLOCKS[0] as ITargetBlock<Feed>).Post<Feed>(feed);
  4.         }

So we are getting a reference to the first block in the pipeline and “Posting” the feed to it.  By virtue of the linking we saw earlier, the rest of the system “just works” by passing the data through the pipeline.

Posted in: .Net | TPL Dataflow | Natural Language Processing

Tags: , ,

Using AlchemyAPI to Classify Blog Posts–Part 2

August 14, 2012 at 5:02 PMAdministrator

In my last post, I explained how to download the API classes from AlchemyAPI and how to make a couple of modifications to make the API classes work better in our highly-async / parallel application.  We'll achieve the asynchronous behavior of our application by using the TPL Dataflow library.

What is TPL Dataflow?

The TPL (Task Parallel Library) was introduced by Microsoft to enable an easier model for working with asynchronous and parallel processing .  Instead of working directly with threads, locks and other low-level constructs, the developer can wrap the logic to execute with a Task instance.  The Task provides the ability to control how the logic runs in regard to how many threads and instances will be used, how the work is scheduled on the CPU, what should be executed when the Task is complete and more.

Dataflow is built on top of the TPL to create an even higher-level abstraction specifically for cases where we have data and we want that data to flow through a series of processes.  Each constituent part of the processes can be wrapped with a Task, but it also gets the benefit of thread safe input and/or output buffers.  The Dataflow library contains a collection of "Block" types and Buffers.  Each block can be a Source, a Target or both.  We wrap a single delegate with one of these blocks and our delegate can then either generate and / or consume pieces of data.  We can link these blocks together such that one block generates data and passes it on to one or more blocks downstream for further processing.

I'm oversimplifying Dataflow, here, but you can learn more about it at this site.

Solution Structure

As I said, the application will largely consist of delegates wrapped in Dataflow blocks, which are then linked together into a kind of pipeline.  Below is an illustration of how these blocks will be arranged and the purpose each has.

 

                                 image

Now, since we can link the blocks in any way we choose (keeping in mind whether the block is a source or target, of course), we could have arranged these blocks in any way we choose.  I elected to just line my blocks up into a pipeline type configuration.  I could have had the Feed download block pass its data off to the other blocks in parallel, but in test runs I found the CPU was idle a good deal of the time due to time spent in network calls.  Adding the additional parallelism seemed like overkill to me.

AlchemyAPI provides the ability to retrieve different pieces of data about the blog post and each is obtained in a separate call.  Here, I've created a block to wrap each call. 

Let's take a look at each of the blocks in more detail so you can get a feel for how all this works together.

  • Feed Download Block – This block wraps a delegate that takes the URL of the blog feed and retrieves each of the blog items in the feed.  The block used here is a TransformManyBlock.  This type of block takes an input (so it's a target), transforms that into many outputs (so it's a source, too).  This block's input buffer will consist of strings representing the URL's to the blog feeds and it's output buffer will consist of blog items being passed on to the next block.  Since one input produces so many outputs, for this block, I chose to have it run on only a single thread.  It should be noted that the developer can specify the size of the buffers.  Be sure to set the buffers to be large enough to cache all the items that will be passed to your block.  If one block is quickly producing data, it can overrun the buffers of blocks consuming that data.  In this case, TDF just ignores the extra pieces of data.
  • Process Category Block thru Process Relations Block – These blocks wrap delegates that make specific calls to the AlchemyAPI services to obtain a particular type of data pertaining to the post. Upon receiving the data from the call, the block fills in a particular portion of the model's object graph.  For example, there is a class representing the feed item.  That class contains collections of Concepts, Entities, etc.  Since each block is populating  a different collection in the object graph, there is no need to worry about contention on those resources.  Each of these blocks is configured to run on 4 threads, so the output from the Feed Download Block will be passed to 4 instances of the Process Category Block running in parallel.  The block type used to wrap these delegates is a TransformBlock which is similar to the TransformManyBlock except there is a one-to-one relationship between the input and output, here.  Each wrapped delegate takes an object graph representing the information about a particular feed item, adds the information it obtained and then passes that same graph out.
  • Process DBUpdates Block – This block is responsible for taking the entire object graph and persisting it to the database.  It is an ActionBlock because this type of block is a Target, only.  I have configured this block to run on a single thread, as well.  Certainly the database access could be completed in a more speedy fashion if this block were to run on many threads, but I did not want to have duplicate pieces of information in the database.  For example, if an Entity was discovered in one feed item, it is quite possible it was also found in another item.  When this occurred, I wanted the first instance to be written to the database and each time it was referenced afterward, a link to the existing instance would be established with the second feed item.  Running this block on multiple threads would possibly allow duplicates to be inserted since one thread would have no way of knowing what another thread was writing to the database.

Conclusion

Since we made simple changes to the API classes to allow for asynchronous processing while calling the AlchemyAPI services, this approach to building the application is very efficient.  At no time is a thread blocking to wait on a network call to return.  Using the TPL Dataflow allowed a very easy way of achieving a multithreaded application.  Quite literally, the speed with which this application processes your feeds is tied only to how fast the network calls return.

I will be posting the source for this application in the very near future and will also be blogging more on TDF, so stay tuned!

Posted in: TPL Dataflow | .Net | Natural Language Processing

Tags: , ,

Using AlchemyAPI to Classify Blog Posts–Part 1

August 7, 2012 at 11:57 AMAdministrator

Obtaining the API Classes

To start using AlchemyAPI, go here to sign up for a user key.  After filling in the form, a key will be mailed to you that can be used for 1000 API calls per day.  Next, you'll want to download the .Net classes you can use to simplify the calls to the API.  You can obtain the classes, here.  What you receive, in the zip file, is the source code for the classes but no project or solution file.  So, open Visual Studio and create a new Class Library project.  Then, import the classes you downloaded into this new project.  You should end up with something like below:

Alchemy

You'll notice the top class, AlchemyAPI, and then several "*Params" classes.  The first class contains methods for making the various calls to the API and each type of call has two overloads.  One uses the default configuration for the call and the other allows you to override the configuration by passing in one of the "*Params" classes.  You can find the default values for each call in the AlchemyAPI documentation for the particular call.  If you wish to override any of the defaults, simply populate the proper params class and pass it into the call.

Going Async

I made one change to the classes supplied.  Since I used TPL Dataflow to get parallel execution, I didn't want any threads blocking while waiting on a network call to return.  For this reason, I added some async and await code.

In the AlchemyAPI class, near the bottom, you'll find a method called DoRequest().  This method is used to make any API calls using an instance of WebRequest.  To make the calls async, I first changed the method signature by adding the async keyword.

DoRequest Signature
  1. private async Task<string> DoRequest(HttpWebRequest wreq, AlchemyAPI_BaseParams.OutputMode outputMode)

Next, I simply changes the actual web call to the following.

Async Call
  1. using (HttpWebResponse wres = wreq.GetResponse() as HttpWebResponse)
  2.             {
  3.                 StreamReader r = new StreamReader(wres.GetResponseStream());
  4.  
  5.                 xml = await r.ReadToEndAsync();

The above prepares the API for our purposes and puts us in a position to begin constructing the rest of our application.  Next time, we'll look at TPL Dataflow and how it can be used in this scenario and how the application was designed around TDF's capabilities.

Posted in: .Net | Natural Language Processing

Tags: ,

NLP at work in AlchemyAPI.com

July 31, 2012 at 10:57 AMAdministrator

I ran across AlchemyApi about a year ago and implemented a service that digested all the feeds I'm subscribed to in Google Reader and saved meta data about each feed post in a database.  This allowed me to search the database from a Silverlight front end and look at posts with a given keyword, category, etc.

What is AlchemyAPI?

This is an API to which you can pass text or the URL to a web page and what you get back, depending on the service method you call, are entities, keywords, categories, concepts, relation and more.  The service uses NLP to process the text or web page and extract the asked for values. 

How is it useful?

It would seem to me the primary use for such functionality would be to have it parse the text of a piece of content and the returned meta-data would be used to tag certain words or phrases such that a user could hover over the text or click it and see information about it.  Text specifying companies, people or technologies, for example, could contain information and links to further define the item

Since the service returns keywords and categories, it's also useful for the classification of a body of text.  This is what I plan to use the service for.  I am subscribed to over 500 feeds in Google Reader and it takes a considerable amount of time to browse all the posts for a given day.  I'm currently working on a project that will process all the posts from each feed and save the meta-data about each to a database.  This should, effectively, create a knowledge store I can search from a web application in order to research topics of interest.  I will also be using it to expand the information my smart home knows about by mapping the entities to tokens inside the Language parser.  Also, the relations (RDF triplets) form statements that can be retrieved when the user asks a question of the smart home system.  I'm rewriting the implementation of this application to provide this integration and also to take advantage of TPL Dataflow to parallelize the processing.

Details

By just signing up, you get 1000 API calls a day.  They offer plans to support whatever level of service you require.  The services are implemented as web API's, but they provide a set of .Net classes that simplify making the calls.

In the next few posts, I'll be writing about details of getting set up to use the service and how I'm using it in the application I mention above.  I'll also be sharing the code for this application when it gets to a point where it makes sense to do so.

Posted in: .Net | Natural Language Processing | Smart Home

Tags: , ,

Open Source NLP Engine

July 25, 2012 at 6:35 AMAdministrator

Over the past several months, I have been asked many times to consider releasing the Natural Language processor I have written to the community as an open source project. While I am not quite ready to commit to doing this, I do have good news for those of you who are interested in obtaining source code to an engine very similar to mine.

Ian Mercer, whose NLP engine inspire my own, has decided to release the source code for his engine as open source. You can find the project on GitHub.

For those of you who have been asking for access to a project like this, good luck and I hope you enjoy digging into this project.

Posted in: .Net | Natural Language Processing

Tags: ,

Slides from Atlanta Code Camp

May 21, 2012 at 1:39 PMAdministrator

Here are the slides from my Natural Language Processing talk at Atlanta Code Camp, last Saturday.

 

 

NLPCSharp.zip (519.41 kb)

Posted in: .Net | Natural Language Processing

Tags: ,

Presenting this Saturday at Atlanta Code Camp

May 17, 2012 at 7:50 AMAdministrator

I'll be presenting a talk this Saturday at the Atlanta Code Camp about the natural language processing engine I've been blogging about, recently. You can find more details at http://www.atlantacodecamp.org/default.aspx.

Hope to see you there!

Posted in: .Net | Natural Language Processing

Tags: ,