Preface

This tutorial discussing the basics of building a receptor.  In the tutorial, you'll see some paragraphs where I identify tasks that would improve the HOPE user experience.  These are marked with "NEED:"  I hope you find this tutorial helpful -- more will be posted soon!

Introduction

When writing a receptor, there are some basic patterns that are always followed.  First, consider the questions:

  1. Does your receptor process semantic types?
  2. Does your receptor emit semantic types?
  3. Will your receptor need a user interface for visualizing the semantic data?
  4. Will your receptor require or offer some user configurability?
  5. What other receptors will your receptor interact with?

Receptors typically implement a very specific behavior.  From the perspective of the user, it is the "system of receptors" that creates a rich and flexible application, therefore be careful that your receptor does not do "too much."

For example, let's write a very basic receptor that loads a text file that the user specifies in a configuration UI.  All the receptor does, either when it's initialized or when the user selects the text file, is load the file into memory and emit it as a "Text" semantic type.  We already have receptors for both displaying text semantic types and speaking them, so we let the user configure what he/she wants to do with the text. 

Later on:

  1. We'll look at a more advanced topic -- the idea of the user creating a base semantic type so that the text file has more semantic meaning than just "text" as this opens the door to more interesting kinds of semantic processing.
  2. We'll write a translate receptor which will interface, probably with Google's translate API, to translate the text file.
  3. We will also feed the text to the AlchemyAPI for NLP processing.

As you can see, starting with a very basic receptor, we can create different applets that do some interesting and meaningful things with even the most basic of semantic types, "text."

Getting Started

Creating the Basic Project

Assuming you are going to write a receptor available to the public, open Visual Studio and create the receptor project in the "Receptors" folder or in a sub-folder if you feel that is appropriate:

We'll call our receptor the "TextFileLoaderReceptor".

In the "New Project" dialog:

  • select Class Library
  • enter in the name of your receptor
  • make sure the location is correct ("Receptors" folder and any sub-folder you may want)

Visual Studio will create a class.  Rename this class and its associated file to "TextFileLoader" (removing the word "Receptor"):

Setup References

Once the project is created, you should add some references right away:

  • Clifton.Receptor.Interfaces
  • Clifton.SemanticTypeSystem.Interfaces
  • lib

These are the core assemblies that every receptor should reference.

Lastly, add references to these two assemblies in your receptor's .cs file:

using Clifton.Receptor.Interfaces;
using Clifton.SemanticTypeSystem.Interfaces;

Because we'll be working with a file chooser dialog and a file reader, we'll also add the System.Windows.Forms reference and add:

using System.IO;
using System.Windows.Forms;

to the code.

We'll also add this "using" statement so we get access to the XML instantiation engine's attributes, which we use later on:

using Clifton.MycroParser;

So, all put together, we have:

using System;
using System.IO;
using System.Text;
using System.Windows.Forms;

using Clifton.MycroParser;
using Clifton.Receptor.Interfaces;
using Clifton.SemanticTypeSystem.Interfaces;

namespace TextFileLoaderReceptor
{
public class TextFileLoader : BaseReceptor
{
... etc...

Setup Post-Build Event

Assuming you are building the receptor in the HOPE solution, you'll want to copy the receptor's assembly to the HOPE application's bin\debug folder.

NEED: This step would not be necessary if the HOPE application had a configuration file of folders to look into for receptors, so that it could scan the folders and know where the receptor's assembly and any optional UI configuration file lived.

In the post-build step, copy the receptor assembly the the bin\Debug folder of the HOPE application, which is called "TypeSystemExplorer".

NEED: The application name needs to be renamed to "HOPE".

For example:

copy TextFileLoaderReceptor.dll ..\..\..\..\TypeSystemExplorer\bin\Debug

Setup the Receptor Class

All receptors derive either from:

  • BaseReceptor
  • WindowedBaseReceptor

"WindowedBaseReceptor" is a useful base class for when your receptor has a "visualization" UI (as opposed to a configuration UI, which is handled by "BaseReceptor.")

For our purposes, since there's no visualization UI, we'll use BaseReceptor and create the required constructor, which takes the IReceptorSystem interface as a parameter and passes it to the base class:

Also, you must implement the Name property:

public override string Name { get { return "Text File Loader"; } }

Build your receptor, then...

Add Your Receptor to the Receptor Chooser List:

In HOPE application ("TypeSystemExplorer" for now), open the Controllers/ReceptorChooserController.cs file:

NEED: When implementing the "auto-detect receptors from specified folders" feature, the receptor chooser should be auto-populated with discovered receptors.

Add a ReceptorEntry to the collection.  In our case:

receptors.Add(new ReceptorEntry() { Name = "Text File Loader", Filename = "TextFileLoaderReceptor.dll" });

Run The Application

At this point, you can run the application and, in the applet designer, add your new receptor to the surface!

Of course it doesn't do anything, but you do now actually have a receptor ready for its unique implementation.

Receptor Implementation

In this section, we'll implement the receptor behavior:

  • Create a configuration UI
  • Handle initialization and file selection
  • Load the file and emit the text as a Text semantic type

What Signals do we Emit?

Receptors should inform the application as to what semantic types they emit, usually in the constructor.  Here, we tell the application that we are emitting the Text semantic type:

public TextFileLoader(IReceptorSystem rsys)
: base(rsys)
{
AddEmitProtocol("Text");
}

Configuration UI

While you can implement a receptor configuration UI as a form in the receptor itself, the preferred way to do this is by using an XML file.  HOPE automatically persists designated configuration properties and it also somewhat separates the UI from the receptor implementation.

The first step is to specify the name of the configuration UI.  In our case:

public override string ConfigurationUI { get { return "TextFileLoaderConfig.xml"; } }

Next, we implement the UI.  Here we'll create a UI that lets the user enter a file path or use a the built-in .NET file browser.  At the moment, all configuration UI's must live in the application's bin\Debug folder.

NEED: With the receptor discovery feature, the configuration and visualization UI's should also be associated, such that they do not need to live in the bin\Debug folder.  This requires reworking where the application  goes to load the XML files.

Standard pieces of the configuratio UI are:

  • the enabled/disabled checkbox
  • the Save/Cancel buttons
  • the default button behavior
<wf:CheckBox def:Name="ckEnabled" Text="Enabled?" Location="20, 95" Size="80, 25"/>
<wf:Button def:Name="btnSave" Text="Save" Location="360, 10" Size="80, 25" Click="OnReceptorConfigOK"/>
<wf:Button def:Name="btnCancel" Text="Cancel" Location="360, 40" Size="80, 25" Click="OnReceptorConfigCancel"/>

<wf:Form ref:Name="form" AcceptButton="{btnSave}"/>
<wf:Form ref:Name="form" CancelButton="{btnCancel}"/>

In our case, we'll add the ability for the user to specify either enter a file path or browse.  The complete XML looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
xmlns:r="Clifton.Receptor, Clifton.Receptor"
xmlns:def="def"
xmlns:ref="ref">
<wf:Form def:Name="form" Text="Text File Loader Configuration" Size="480, 160" StartPosition="CenterScreen" ShowInTaskbar="false" MinimizeBox="false" MaximizeBox="false">
<wf:Controls>
<wf:Label Text="File Name:" Location="20, 20" Size="100, 15"/>
<wf:TextBox def:Name="tbFileName" Location="20, 35" Size="200, 20"/>
<wf:Button def:Name="btnFileChooser" Text="Browse..." Location="230, 32" Size="80, 25" Click="{receptor.ShowFileChooser}"/>
<wf:CheckBox def:Name="ckEnabled" Text="Enabled?" Location="20, 95" Size="80, 25"/>
<wf:Button def:Name="btnSave" Text="Save" Location="360, 10" Size="80, 25" Click="OnReceptorConfigOK"/>
<wf:Button def:Name="btnCancel" Text="Cancel" Location="360, 40" Size="80, 25" Click="OnReceptorConfigCancel"/>
</wf:Controls>
<r:PropertyControlMap def:Name="ControlMap">
<r:Entries>
<r:PropertyControlEntry PropertyName="FileName" ControlName="tbFileName" ControlPropertyName="Text"/>
</r:Entries>
</r:PropertyControlMap>
<wf:Form ref:Name="form" AcceptButton="{btnSave}"/>
<wf:Form ref:Name="form" CancelButton="{btnCancel}"/>
</wf:Form>
</MycroXaml>

We add a property in our receptor that gets automatically wired up to the configuration UI TextBox via the entries in the PropertyControlMap:

[UserConfigurableProperty("Filename:")]
public string FileName { get; set; }

Note that this is a one-way property binding. 

We also implement clicking on the "Browse..." button.  Note that the Click handler is automatically wired up, where "receptor" is the receptor instance that is associated with this configuration dialog.  In other words, when you double-click on a receptor to configure it, that receptor receives UI events wired up in the XML.  Because the property binding is one way, we need to auto-populate to the control in the receptor itself:

[MycroParserInitialize("tbFileName")]
protected TextBox tbFileName;

so we can assign the file browser's selection to the textbox:

protected void ShowFileChooser(object sender, EventArgs args)
{
FileDialog ofd = new OpenFileDialog();
ofd.Filter = "Text Files (.txt)|*.txt|All Files (*.*)|*.*";

if (ofd.ShowDialog() == DialogResult.OK)
{
tbFileName.Text = ofd.FileName;
}
}

The final configuration UI looks like this:

It gets the job done.

Loading the File and Creating the Semantic Signal

We override the method UserConfigurationUpdated to implement our file loader.  Here we have the opportunity to return whether the process was successful and any associated error message:

public override bool UserConfigurationUpdated()
{
base.UserConfigurationUpdated();
bool ret = LoadAndSendFileText(FileName);

return ret;
}

protected bool LoadAndSendFileText(string filename)
{
bool ret = false;

try
{
string text = File.ReadAllText(filename);
SendTextSignal(text);
ret = true;
}
catch (Exception ex)
{
ConfigurationError = ex.Message;
}

return ret;
}

public void SendTextSignal(string text)
{
CreateCarrier("Text", signal => signal.Value = text);
}

Loading the File on Startup

Lastly, we override EndSystemInit() to load the file on startup that the user specified in the configuration:

public override void EndSystemInit()
{
base.EndSystemInit();
LoadAndSendFileText(FileName);
}

Because the FileName property was decorated with the UserConfigurableProperty:

[UserConfigurableProperty("Filename:")]
public string FileName { get; set; }

the value of this property is automatically persisted when the user saves the applet, and is restored on load.  Note that the parameter to this attribute is currently ignored.

Now we can add the Text Display receptor to create a two-receptor applet that displays the Text semantic type.  If we create a text file called "hi.txt" with the context "Hello HOPE!", it will now be displayed in the text display receptor:

Receptor Subtitles

One final touch is to display the filename portion as a subtitle of the Text File Loader receptor.  "Subname" is a built-in property of the base class:

protected bool LoadAndSendFileText(string filename)
{
Subname = Path.GetFileName(ofd.FileName);
... etc...

Text to Speech

Our receptor can also take advantage of the existing Text to Speech receptor - simply add it to the applet surface and it will receive the Text signal as well:

Separate Tabs

We can load text files into separate tabs by creating "computational islands" with membranes:

Of course, it would be great if the tab title reflected to filename -- we'll leave this for later!  But the salient point here is that we can put together simple receptor combinations into different configurations depending on the user's needs.

Conclusion

This concludes the introductory tutorial on how to write a receptor.  We'll explore receiving signals next.