-
Notifications
You must be signed in to change notification settings - Fork 2
Getting Started Guide
This document provides a guide for getting started with developing with the CENode library and assumes a general knowledge of
- The CE (Controlled English) dialect
- The CECard protocol
- CEStore and CENode goals
The official documentation outlines key use-cases and gives an overview of the APIs exposed by the library. However, please note that code snippets in this document may be out-of-date.
In this guide, we will use CENode in the setting of a web application that will allow a user to conduct a simple conversation with a local agent.
In this scenario, the CENode library is used in a client-side webpage. However, the same JavaScript code can be used regardless of where CENode is used (e.g. in a Node app).
A complete example demonstrating the result of this guide is available on the CENode website.
The complete example goes no further than the code in this guide, other than to add some simple styling and some instructions on use.
Create a new directory in your normal project space and change into it.
$ mkdir ~/Project/MyCENodeProject
$ cd ~/Project/MyCENodeProjectWe will put all our code into this directory.
Within the project, create a further directory for your JavaScript files, and create an empty HTML file that will form the base of the webapp:
$ mkdir js
$ touch index.htmlIf you would like to place this project under Git version control, then initialise the repository:
$ git initTo make an initial commit, then first ensure your global Git settings have been properly set:
$ git config --global user.name "My Name"
$ git config --global user.email "myname@mydomain.com"Now you can continue to stage your files and commit them:
$ git add .
$ git commit -m "Initial commit"If you have a remote repository you'd like to push to, then you'll need to create one and specify this within Git. Checkout this guide on how to do this with GitHub.
If you already have Node and NPM installed, then skip this step.
We use NPM to manage dependencies and installation for CENode, and so it should be installed onto your system before continuing.
For example, on macOS, this can be accomplished using brew:
$ brew install node
This step is optional, but using a package.json file is useful for managing your project, keeping scripts handy and to keep track of dependencies.
To create one, run:
$ npm init
The only dependency our app has (unless you later require more) is CENode itself, which can be installed using NPM.
$ npm install --save cenode
The --save flag tells NPM to add CENode to your package.json.
NPM modules are installed to a directory called node_modules. You should add this directory to your .gitignore file to prevent it from being included in your source control:
$ echo "node_modules/" >> .gitignore
Now, since CENode has been added to your package.json file, the next time you need to pull your project to a new machine, you can simply run npm install to add re-all your project dependencies (including CENode).
We will write all of our app logic within a file named main.js, so create this:
$ touch js/main.jsCreate the skeleton of the app by ediing index.html:
<!DOCTYPE html>
<html>
<head>
<title>My CENode app</title>
</head>
<body>
<h1>My app</h1>
<script src="node_modules/cenode/dist/cenode.min.js"></script>
<script src="node_modules/cenode/dist/models.js"></script>
<script src="js/main.js"></script>
</body>
</html>If you open this page in a web browser, you'll see a plain page aside from the heading and the title.
You can now start to write some code that uses the CENode library. Start by initialising the library with some core data and attaching and setting the node's local agent name:
const node = new CENode(CEModels.core); // The CEModels object is provided by the 'models.js' file included above
node.attachAgent(); // Initialise an agent to act on this Node
node.agent.setName('agent1');This code creates an instance of CENode and turn spins up a CEAgent, which continuously runs in the background and is able to respond to certain events, as we'll cover later.
Note that we are using standard ES6 syntax here (e.g. const). While this is generally fine, some older browsers may not work correctly with this syntax. If this is a problem, then consider reverting to ES5 or use a tool like babel.
Notice that we have passed a variable CEModels.core to the constructor. This model allows the CENode to initialise itself with any key concepts and instances that are required, and means you don't need to manually enter sentences in one at a time in order to achieve the same thing.
These types of models are actually very simple, and are simply a JavaScript array of CE sentences that are fed in in order to the node. You might decide to create your own model for a particular domain that allows the node to have some basic knowledge of the domain's 'world' before you even start working with it.
If, for example, you are using CENode to maintain knowledge about space, you might create your own model for this:
const myModel = [
"there is a rule named 'r1' that has 'if the planet C ~ orbits ~ the star D then the star D ~ is orbited by ~ the planet C' as instruction",
"there is a rule named 'r2' that has 'if the planet C ~ is orbited by ~ the moon D then the moon D ~ orbits ~ the planet C' as instruction",
"conceptualise a ~ celestial body ~ C",
"conceptualise the celestial body C ~ orbits ~ the celestial body D and ~ is orbited by ~ the celestial body E",
"conceptualise a ~ planet ~ P that is a celestial body",
"conceptualise a ~ moon ~ M that is a celestial body",
"conceptualise a ~ star ~ S that is a celestial body",
"there is a star named sun",
"there is a moon named 'the moon'",
"there is a planet named Earth that orbits the star 'sun' and is orbited by the moon 'the moon'"
];Any number of models can be passed to CENode when initialised, e.g.:
const node = new CENode(CEModels.core, myModel);(We pass the core model before the custom one, because the rule concept is created by the former. If we didn't do this, then the rules we created in our custom model would be ignored. The core model also adds support for CECards, which we'll need to use later.)
We don't need to pre-populate the node in this way if you don't want to, but it might give you and the agent a bit more to talk about if you do.
To support the conversation between the human and the agent, we need to build three things;
- A means for inputting sentences
- A means for displaying messages from the agent
- Code to wire up the interface to the agent
To start, let's build a basic interface by adding some standard HTML components to index.html's <body>:
<textarea id="input"></textarea>
<button id="send">Send message</button>
<ul id="messages"></ul>You may like to style these elements, but that will not be covered in this guide.
Next, we need to respond to clicks of the button. After the button is pressed, we need to wrap the input message into a CECard addressed to the local agent. The card also needs to declare who it is from, so the agent can respond, if necessary. Let's first declare a variable we'll set to hold our own name in, and then a function that is called when the button is pressed.
Place this code in main.js after the node has been initialised and the agent name has been set. We will declare our own name, grab references to the key DOM elements we'll need to later interact with, and also create a function that responds to button presses.
const myName = 'User';
const input = document.getElementById('input');
const button = document.getElementById('send');
const messages = document.getElementById('messages');
button.onclick = function(){
const message = input.value;
input.value = ''; // blank the input field for new messages
const card = "there is a nl card named '{uid}' that is to the agent 'agent1' and is from the individual '"+myName+"' and has the timestamp '{now}' as timestamp and has '"+message.replace(/'/g, "\\'")+"' as content.";
node.addSentence(card);
// Finally, prepend our message to the list of messages:
const item = '<li>'+message+'</li>';
messages.innerHTML = item + messages.innerHTML;
};(Note: we have used special character sequences {uid} and {now} to help us construct the card. CENode will complete these fields for you by generating a unique name for the card and by calculating the timestamp automatically.)
In this code, we take the input message the user created, wrap it in a CECard (of type nl card since we can't guarantee the user's entry will be pure CE), and then add it to the node.
The local agent will soon find this card and, since it is the addressee, open it to parse the contents. If the content is valid CE, the agent will update the CEStore with the new knowledge.
Now that we are able to input messages to the node, we will need to be able to retrieve any responses. By default, the agent will not give very verbose responses to input unless we tell it to. We can write a policy that tells the agent to tell us more information or a more detailed response to our inputs (which may be questions).
To do so, add the following sentence to a custom model passed to the node during initialisation:
const myModel = [
...
"there is a feedback policy named p1 that has 'true' as enabled and has the individual '"+my_name+"' as target and has 'full' as acknowledgement"
...
];Remember to pass this model to CENode when initialising it along with the core one.
CEAgents work entirely asynchronously to the rest of the app and the CENode KB itself, and we don't want to block the app whilst we wait for a response. Therefore, we need to write a method that continuously polls the CENode for any cards that the CEAgent may have written back to us.
Let's write a function, below the rest of the code in main.js that continually runs, checking for new cards:
const processedCards = []; // A list of cards we've already seen and don't need to process again
function pollCards(){
setTimeout(() => {
const cards = node.getInstances('card', true); // Recursively get any cards the agent knows about
for(const card of cards){
if(card.is_to.name == myName && processedCards.indexOf(card.name) == -1){ // If sent to us and is still yet unseen
processedCards.push(card.name); // Add this card to the list of 'seen' cards
const item = '<li>'+card.content+'</li>';
messages.innerHTML = item + messages.innerHTML; // Prepend this new message to our list in the DOM
}
}
pollCards(); // Restart the method again
}, 1000);
}The above function will call itself every 1000 milliseconds (1 second). We need to add one more line to the bottom of the main.js script that makes sure the pollCards() repeating function is called when the app starts:
pollCards();And that's it - we have a very basic app using CENode to support a simple conversation between a human and the machine. Refresh the page in your browser and you should be ready to start talking.
You may notice that we can access properties of instance objects in different ways. These are related to the way instances are maintained by the node. At any time, you can inspect a particular instance object by logging it and then inspecting your browser's JavaScript console:
console.log(card);Properties such as name can be accessed directly, e.g.:
const name = card.name;name is a name given to the instance. Sometimes this might be a simple identifier (e.g. msg_23 for instances of type card) and sometimes it might be a more human-readable name (e.g. agent1).
Information about properties of a particular type can easily be queried in CE:
what is a celestial body?
There are two types of instance values:
- A reference (with a label) to another instance in the node's KB
- A labelled string
Both types of values (e.g. with label 'label') can be retrieved with code similar to (and used above):
const value = instance.label;or
const value = instance.property('label');In the case of the former, value will contain another instance object, which in turn has its own name, values and relationships.
With the latter, value will simply be a string. An example of this is a card's content value (as shown above).
Relationship properties are handled in a very similar way to values, except that all relationship properties refer to another instance object (again with a label called 'label'):
const rel = instance.label;or
const rel = instance.property('label');As such, rel will be an instance object with its own names, values, and relationships. This is why we need to access the name property of the instance returned when checking the is to relationship above.
Clearly this is a very basic app that supports simple chat functionality. We haven't added any support for confirming CE that the agent has guessed from our NL inputs (although valid CE will be autoconfirmed by the agent).
Remember to checkout the complete demo for a complete implementation of the code covered in this guide.