Compare commits
96 Commits
master
...
v3-updates
Author | SHA1 | Date | |
---|---|---|---|
|
372c9c4941 | ||
|
93513a42b6 | ||
|
6fcc8d4508 | ||
|
6f335609f6 | ||
|
24cbe22ac7 | ||
|
cad66fffbe | ||
|
be38274eb7 | ||
|
aaad163ed9 | ||
|
6916a2bfa0 | ||
|
6b340a8b7b | ||
|
02cb4e1400 | ||
|
4692478719 | ||
|
8976b9544c | ||
|
1c644219fd | ||
|
43d61a909c | ||
|
c48bcd403f | ||
|
195b082261 | ||
|
0af13a951b | ||
|
600c08d223 | ||
|
b10c6be7c4 | ||
|
0205f12ab8 | ||
|
5b3784947e | ||
|
8c8d598c00 | ||
|
b84d5ce709 | ||
|
4ac0c2b696 | ||
|
aabd510b61 | ||
|
5ed7886e93 | ||
|
12ac2e6dd5 | ||
|
f090b875aa | ||
|
7e9cee3a4d | ||
|
0417d39d88 | ||
|
36ec8c80b7 | ||
|
9ffb996771 | ||
|
86611ba13d | ||
|
1690ebbef3 | ||
|
c0d8d4c938 | ||
|
372023e4d8 | ||
|
a1310118dc | ||
|
f1237be883 | ||
|
6d305de736 | ||
|
28d36630ea | ||
|
0536ea693b | ||
|
f8aa6a260c | ||
|
e81f7a2d69 | ||
|
190d3412a3 | ||
|
81c727fe3f | ||
|
1734d023ab | ||
|
33ed6ea06f | ||
|
0990bb013e | ||
|
074f12c0c8 | ||
|
c9529ef9c1 | ||
|
7c6af583b1 | ||
|
cb7169b7de | ||
|
ed2f1ffbe9 | ||
|
edc63bbacb | ||
|
872fc75b58 | ||
|
089690768a | ||
|
aeca4e4211 | ||
|
1bf3578438 | ||
|
4611725fe9 | ||
|
c4f4de09f3 | ||
|
9af8abcb3c | ||
|
6687957025 | ||
|
320d5c46c1 | ||
|
7d55cff045 | ||
|
c22ed513cd | ||
|
cb4dcbfb95 | ||
|
9babfdf2ff | ||
|
2f97b78c4a | ||
|
48ce757df5 | ||
|
ddda963d95 | ||
|
b9371d8218 | ||
|
90f3e71d50 | ||
|
ba282ef8e2 | ||
|
0b41d9637d | ||
|
14bb3e35a7 | ||
|
a2fabff474 | ||
|
da9ec78a95 | ||
|
c1f764b7d1 | ||
|
a3a62be374 | ||
|
cb93cef889 | ||
|
9c65dd752a | ||
|
a280dd13a1 | ||
|
3b72a5e87f | ||
|
abc91ab501 | ||
|
b2a0123063 | ||
|
c3642d606f | ||
|
ebe69bf6bf | ||
|
7be0bface5 | ||
|
071e2a2dd9 | ||
|
c03df3bfb8 | ||
|
fa9fb276a2 | ||
|
5101a932ba | ||
|
7e76a4cb19 | ||
|
6c66cb36d3 | ||
|
bf2f0c13bb |
18
.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"max-len": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"guard-for-in": 0,
|
||||
"global-require": 0,
|
||||
"lines-around-directive": 0,
|
||||
"strict": 0
|
||||
},
|
||||
"extends": "airbnb-base"
|
||||
}
|
4
.gitignore
vendored
@ -1,3 +1,7 @@
|
||||
testing.js
|
||||
node_modules/
|
||||
lib/
|
||||
dist/
|
||||
*.sw*
|
||||
.DS_Store
|
||||
*.bbl
|
||||
|
12
.webpackrc.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
entry: './lib/CENode.js',
|
||||
entry: {
|
||||
CENode: './lib/CENode.js',
|
||||
CEModels: './models/index.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'dist/[name].js',
|
||||
library: '[name]',
|
||||
libraryTarget: 'var'
|
||||
}
|
||||
}
|
298
API.md
@ -1,298 +0,0 @@
|
||||
# CENode API Documentation
|
||||
|
||||
This document outlines the APIs provided by the CENode library when used programmatically and as a web service.
|
||||
|
||||
In this document, the term CENode is used to refer to the entire system (comprising of a CENode knowledge base - KB - and a CEAgent). It also may be used to refer to the actual CENode class and objects of this type. However, the distinction should generally be clear.
|
||||
|
||||
The node maintains a set of concepts and instances, which are represented respectively by the objects of the CEConcept and CEInstance classes.
|
||||
|
||||
## Programmatic API
|
||||
|
||||
The programmatic API is accessed through `CENode`, `CEAgent`, `CEConcept`, and `CEInstance` classes. A standard CENode application would usually use one `CENode` object, which would be maintained by a single `CEAgent` object, and would provide references to any number of `CEConcept` and `CEInstance` objects.
|
||||
|
||||
An application using the library would never instantiate a non-`CENode` CENode object, as this class is itself responsible for generating objects on your behalf.
|
||||
|
||||
### CENode class
|
||||
|
||||
Functions and properties of instances of the CENode class.
|
||||
|
||||
#### `CENode CENode([model1[, model2[, model3 ...]]])`
|
||||
Construct a new `CENode` object. This is the only class of the library one should directly instantiate.
|
||||
|
||||
`var node = new CENode(model1, model2, ...)`
|
||||
|
||||
Constructing this object starts the lifecycle of a CEAgent, whose name is, by default, set to 'Moira'.
|
||||
|
||||
Any number of models (string arrays) can be passed upon instantiation in order to pre-populate the node's KB. See the documentation for more details.
|
||||
|
||||
#### `[CEInstance] get_instances([concept_type, [recurse]])`
|
||||
Retrieve an array of CEInstances from the node, representing the instances currently known and maintained by the node's KB.
|
||||
|
||||
Optionally specify the concept type (str) to retrieve for, and specify whether you'd like to recurse (bool) to get instances of all children of the specified type.
|
||||
|
||||
For example, `get_instances('card', true)` returns all instances of concept 'card' and all descendants of the type 'card'.
|
||||
|
||||
When called without any arguments, this function returns all instances known by the node.
|
||||
|
||||
#### `[CEConcept] get_concepts()`
|
||||
Retrieve an array of CEConcepts from the node, representing all of the concepts currently known and maintained by the node's KB.
|
||||
|
||||
#### `str guess_next(input)`
|
||||
Retrieve a string representing an attempt at a guess of what might be next in the input string.
|
||||
|
||||
For example, if the node knows a concept called 'person', then providing an input such as 'there is a pe' might return 'there is a person named '.
|
||||
|
||||
#### `add_sentence(input)`
|
||||
Add a new sentence to be processed by the node. Internally, this function dynamically tries to evaluate the input string in the following order of events. If one stage fails, the function attempts the next:
|
||||
|
||||
* Attempt to parse the input string as valid CE. If successful, then this might result in the creation or modification of a concept or instance.
|
||||
* Attempt to parse the input string as a question.
|
||||
* Attempt to parse the input string as NL.
|
||||
|
||||
Internally, the function uses the function `add_ce()`, `ask_question()`, and `add_nl()` and will return the results from these functions directly. Therefore see documentation on these functions for information on returned data. However, this function will not specify if the evaluation passed or failed, as parsing NL will always return some kind of response.
|
||||
|
||||
This function will evaluate `{now}` and `{uid}` placeholders in the input string to be a timestamp and locally-unique instance ID respectively. Typically, these are used in constructing new cards:
|
||||
|
||||
`there is an nl card named 'msg_{uid}' that has the timestamp '{now}' as timestamp and is from the user 'Will' and is to the agent 'Moira' and has 'Hello, world' as content.`
|
||||
|
||||
#### `add_ce(input[, dryrun])`
|
||||
Add a CE sentence to the node. This may cause the node to create or modify an instance or a concept, and should be the only method used for inserting information into the KB.
|
||||
|
||||
This function returns a standard object containing these fields:
|
||||
|
||||
* `success` - a boolean representing whether or not the node's CE-parser thinks the input is valid CE.
|
||||
* `data` - a string representing the response, if any, from the node. If `success` indicates a failure, this value might provide more insight.
|
||||
* `result` - where appropriate (e.g. successful), this field will contain the CEConcept or CEInstance that was created or modified as the result of the input CE.
|
||||
|
||||
The optional dryrun argument is a boolean that will still evaluate the input and return the same values, but will not actually update the KB. This might be useful for detecting if an input is considered to be valid CE before carrying out an insertion. If the dryrun was successful and a CEConcept or CEInstance would have been created or modified, then a projected object of the relevant type will be returned in `result`.
|
||||
|
||||
#### `ask_question(input)`
|
||||
Queries the node's KB for information about a concept or instance.
|
||||
|
||||
This function returns a standard object containing these fields:
|
||||
|
||||
* `success` - a boolean representing whether or not the node understood the question.
|
||||
* `data` - a string representing a response or failure message.
|
||||
|
||||
Several forms of question are understood by the node:
|
||||
|
||||
* what - ask about a particular instance, concept, or property. Returns all information known about the instance, concept, or property
|
||||
* who - synonymous to 'what' but nicer for querying people-like instances
|
||||
* where - ask about the location of a particular instance
|
||||
* what is on/in/at - ask about what is associated with a particular location
|
||||
|
||||
Example questions and responses:
|
||||
|
||||
* Who is Mrs Smith? - Mrs Smith is a teacher. Mrs Smith teaches the class 'B2' and has the subject 'Computing' as subject and has '45' as age.
|
||||
* What is a teacher? - A teacher is a type of person. An instance of teacher teaches a type of class and has a type of subject called subject and has a value called age.
|
||||
* what is teaches? - 'teaches' describes the relationship between a teacher and a subject (e.g. "the teacher 'TEACHER NAME' teaches the subject 'SUBJECT NAME'").
|
||||
* Where is Mrs Smith? - Mrs Smith lives in the house 'Number 23'.
|
||||
* What is in house Number 23? - Mrs Smith lives in the house 'Number 23'.
|
||||
|
||||
Asking a question will not update the node's KB.
|
||||
|
||||
#### `add_nl(input)`
|
||||
Add a natural language sentence to be processed by the node. The node's NL-parser will do its best to try and work out what you mean, and will return a response in a familiar format (a standard object with the following fields):
|
||||
|
||||
* `data` - If successful, a string representing a valid CE sentence based on the input NL, or an error message if otherwise.
|
||||
|
||||
Adding NL will not update the node's KB. However, submitting an NL card containing valid CE will be auto-confirmed by the node's agent.
|
||||
|
||||
#### `add_sentences([inputs])`
|
||||
Add an array of inputs to be processed by the node. This method simply calls `add_sentence()` on each of the inputs, and will return an array of response objects, whose order maps onto the sentences in the input array.
|
||||
|
||||
Since `add_sentence()` is used, the inputs can be a mixture of CE, questions, and NL.
|
||||
|
||||
#### `load_model([model])`
|
||||
Add an array of CE inputs to be interpreted by the node. Internally, this calls `add_ce()` on each of the inputs, and returns an array of such responses in the order expressed by the input order.
|
||||
|
||||
#### `reset_all()`
|
||||
Empty the node's KB of all instances and concepts.
|
||||
|
||||
#### `instances`
|
||||
Directly access the CEInstance representing an instance known by the node. When accessing, use the instance's lower-cased name using underscores instead of spaces. Examples:
|
||||
|
||||
* `node.instances.mrs_smith` - gives the CEInstance of Mrs Smith
|
||||
* `node.instances.moira.name` - gives the string 'Moira' (probably)
|
||||
|
||||
#### `concepts`
|
||||
Directly access the CEConcept representing a concept known by the node. Access in a similar way to instances:
|
||||
|
||||
* `node.concepts.ce_card` - gives the CEConcept representing the 'ce card' concept
|
||||
* `node.concepts.card.name` - gives the string 'card'
|
||||
|
||||
#### `agent`
|
||||
The CEAgent object responsible for maintaining this node.
|
||||
|
||||
### CEConcept class
|
||||
|
||||
Functions and properties of instances of the CEConcept class. Objects of this class represent concepts maintained by the node.
|
||||
|
||||
#### `name`
|
||||
A string representing the name of the concept.
|
||||
|
||||
#### `id`
|
||||
An internally-used ID to help maintain the KB. Generally this can be ignored by applications using the API.
|
||||
|
||||
#### `instances`
|
||||
An array of CEInstances whose type is of the present CEConcept object.
|
||||
|
||||
#### `all_instances`
|
||||
An array of CEInstances whose type is of the present CEConcept object, and any of the descendants of the concept.
|
||||
|
||||
#### `parents`
|
||||
An array of CEConcepts representing the parents of the concept.
|
||||
|
||||
#### `ancestors`
|
||||
An array of CEConcepts representing all ancestors of the concept (parents, grandparents, etc.).
|
||||
|
||||
#### `children`
|
||||
An array of CEConcepts representing the concepts to whom this concept is a parent.
|
||||
|
||||
#### `descendants`
|
||||
An array of CEConcepts representing all descendants of the concepts (children, grandhildren, etc.).
|
||||
|
||||
#### `relationships`
|
||||
An array of standard objects representing the relationships supported by this concept.
|
||||
|
||||
Object is of the form:
|
||||
|
||||
* `label` - a string identifier describing the relationship
|
||||
* `concept` - the CEConcept object the relationship is linked to.
|
||||
|
||||
#### `values`
|
||||
An array of standard objects representing the values supported by this concept.
|
||||
|
||||
Object is of the form:
|
||||
|
||||
* `label` - a string identifier describing the value
|
||||
* `concept` - if the value is associated with another concept, this is the CEConcept object representing the relevant concept. Otherwise this is undefined.
|
||||
|
||||
#### `synonyms`
|
||||
An array of strings representing alternative names for this concept. Any of these names can be used when addressing the concept.
|
||||
|
||||
#### `ce`
|
||||
A string representing the CE sentence(s) that would be needed in order to construct the state of the current concept.
|
||||
|
||||
#### `gist`
|
||||
A string representing a more casual description of the concept. This is returned when asking the question: 'what is <concept name>?'.
|
||||
|
||||
#### Helpers
|
||||
Helpers are provided to allow you to access associated CEConcepts through values and relationships. For example, with the card CEConcept, `card.is_to` will give the CEConcept that the 'is to' relationship is associated with (probably a 'person' or 'agent' concept, depending on your implementation).
|
||||
|
||||
If you are trying to access a value which is not associated with another CEConcept, then instead the helper will just return the string 'value' to indicate that this value should simply be represented by a string and not an instance of a concept.
|
||||
|
||||
### CEInstance class
|
||||
|
||||
Functions and properties of instances of the CEInstance class. Objects of this class represent instances in the KB.
|
||||
|
||||
#### `name`
|
||||
The name string of this instance.
|
||||
|
||||
#### `id`
|
||||
An identifier used to internally recognise this instance.
|
||||
|
||||
#### `sentences`
|
||||
An array of CE sentences that have been provided that have affected this instance.
|
||||
|
||||
#### `type`
|
||||
The CEConcept object that this instance is a type of.
|
||||
|
||||
#### `relationships`
|
||||
An array of standard objects representing the relationships to other instances.
|
||||
|
||||
Objects are of the form:
|
||||
|
||||
* `label` - a string describing the relationship
|
||||
* `instance` - the CEInstance object this instance relates to.
|
||||
|
||||
#### `values`
|
||||
An array of standard objects representing the values held by this instance.
|
||||
|
||||
Objects are of the form:
|
||||
|
||||
* `label` - a string describing the value
|
||||
* `instance` - if the value refers to another instance, then this field holds a reference to that CEInstance. Otherwise this field holds a string.
|
||||
|
||||
Examples of this difference can be observed in the core CE model shipped with the library. CEInstances of type 'card' have a value called 'timestamp' that refers to another instance whose name is the actual value of the timestamp. Cards also have a value called 'content', whose value is a literal string.
|
||||
|
||||
If you're unsure on what type of value you're dealing with, ask about the parent concept (e.g. 'what is a card?'), and the response will describe the various properties supported by the concept.
|
||||
|
||||
#### `property(label[, with_source])`
|
||||
Return the *most recent* value or relationship that has the label with the name specified. If the property is a relationship, then a CEInstance is returned. If it's a value, then either a CEInstance or a string is returned (see the `values` API documentation for more information).
|
||||
|
||||
If `with_source` is defined and `true`, then data will be returned in the format: `{source: SOURCE, instance: DATA}`, where `SOURCE` is the source input of the information (e.g. username), if any, and `DATA` is the returned information when used without the `with_source` flag.
|
||||
|
||||
#### `properties(label[, with_source])`
|
||||
Return an array of CEInstances or strings representing the values or relationships described by the input label.
|
||||
|
||||
As with `property()`, passing a `true` value for `with_source` includes the source input of each piece of information returned, with the output format for each list element as described in the notes for the `property()` function.
|
||||
|
||||
#### `synonyms`
|
||||
A list of strings representing alternative names for this instance. Any of these, or the instance's actual name, can be used when addressing this CEInstance.
|
||||
|
||||
#### `ce`
|
||||
A string representing the CE that would be required to generate the instance in its current form.
|
||||
|
||||
#### `gist`
|
||||
A string representing a more casual description of the CEInstance. This is the text returned when asking questions like 'what is <instance name>?' or 'who is <instance name>?'.
|
||||
|
||||
#### Helpers
|
||||
You can also directly access values and relationships as direct properties of the CEInstance object. For example, for a card instance, `card.is_to` gives the same result as calling `card.property('is to')`. This gives you the latest-reported value or relationship with this name.
|
||||
|
||||
Similarly, `card.contents` gives the same result as calling `card.properties('content')` - all you do is add an extra 's' at the end to access all of the values or relationships reported with that name.
|
||||
|
||||
Note that calling `properties()` and passing a property name that doesn't yet exist for the instance will return an empty array (as expected). However, accessing the information directly (as with `card.is_to`, for example) would return `undefined`, because that property has not yet been defined on the instance.
|
||||
|
||||
### CEAgent class
|
||||
|
||||
Functions and properties of the CEAgent class. Each CENode instance will usually have at least one agent spawned to help maintain it.
|
||||
|
||||
#### `set_name(name)`
|
||||
Set the name of the agent to the specified name.
|
||||
|
||||
#### `get_name()`
|
||||
Get the name string of the agent.
|
||||
|
||||
#### `get_last_successful_request()`
|
||||
If there are policies in place, this function returns the timestamp representing the time at which the last successful connection to another CENode instance occurred.
|
||||
|
||||
Otherwise this returns `0`.
|
||||
|
||||
#### `handle_card(card_instance)`
|
||||
Normally, applications wouldn't need to access this method directly, but it can be useful for asynchronous card-handling.
|
||||
|
||||
This function accepts a fully-constructed CEInstance of a subtype of type card. Currently supported card types include 'ce card', 'nl card', and 'ask card'.
|
||||
|
||||
Internally, this function uses the `add_ce()`, `ask_question()`, and `add_nl()`, functions of the agent's CENode (depending on the type of card submitted), and returns the content from these functions directly once parsed.
|
||||
|
||||
For example, if you submit an ask card with a question in the content, then expect a response consistent with `node.ask_question(question)`.
|
||||
|
||||
Note that the agent will ignore the card if it isn't in the recipient list.
|
||||
|
||||
## HTTP API
|
||||
|
||||
CENode instances can be run as a web service by invoking them directly as a node app:
|
||||
|
||||
```bash
|
||||
$ node cenode.js
|
||||
```
|
||||
|
||||
In these cases, the CEAgent effectively exposes itself to the network and provides HTTP methods to interact with its CENode. Note that the web interface currently only supports minimal interaction with CENode and its components.
|
||||
|
||||
### `GET /`
|
||||
Download a webpage representing a control panel for carrying out simple maintenance on a CENode.
|
||||
|
||||
### `GET /reset`
|
||||
Calls the CENode's `reset_all()` function to empty it of instances and concepts.
|
||||
|
||||
### `GET /cards`
|
||||
Return all cards known by the CENode in line-delimited CE.
|
||||
|
||||
### `POST /agent_name`
|
||||
Send a string representing a new name to assign the agent.
|
||||
|
||||
### `POST /sentences`
|
||||
Send a line-delimited set of sentences to be processed by the node. These use the node's `add_sentence()` method, so each sentence can be a question, CE, or NL.
|
||||
|
||||
The method responds by sending back the data field returned by `add_sentence()` in the same order as the input sentences (such that the data in line 3 of the response corresponds to the input sentence in line 3 of the request body).
|
60
README.md
@ -1,38 +1,62 @@
|
||||
# cenode.js
|
||||
# CENode
|
||||
|
||||
A pure JavaScript implementation of the ITA project's CEStore - called CENode. CENode is able to understand the basic sentence types parsed by the CEStore, such as conceptualising and instance creation.
|
||||
A pure JavaScript implementation of the ITA project's CEStore - called CENode. CENode is able to understand the basic sentence types parsed by the CEStore, such as conceptualising and instance creation and modification.
|
||||
|
||||
Please visit the project's [home page](http://cenode.io) for more information and for documentation.
|
||||
|
||||
See also the [Getting Started Tutorial](https://github.com/flyingsparx/CENode/blob/master/docs/getting_started.md).
|
||||
|
||||
## Installation and use
|
||||
## Getting started
|
||||
|
||||
`cenode.js` can be used in multiple ways.
|
||||
CENode can be imported into your Node apps or run in a browser. Either way, you will need Node and NPM installed before continuing, so install these for your platform first.
|
||||
|
||||
It can be included simply in your web application:
|
||||
Then add CENode to your project using NPM:
|
||||
```
|
||||
<script src="cenode.js"></script>
|
||||
npm install
|
||||
```
|
||||
|
||||
It can be imported into your NodeJS app:
|
||||
```
|
||||
var cenode = require('cenode.js');
|
||||
If using CENode in a webpage, then include it (and models, if necessary) in script tags:
|
||||
```html
|
||||
<script src="/node_modules/cenode/dist/cenode.min.js"></script>
|
||||
<script src="/node_modules/cenode/dist/models.js"></script> <!-- if required -->
|
||||
|
||||
<script>
|
||||
const node = new CENode(CEModels.core);
|
||||
</script>
|
||||
```
|
||||
|
||||
Or it can be run as a standalone NodeJS app:
|
||||
```
|
||||
$ node cenode.js
|
||||
Or, if using in a node app:
|
||||
```javascript
|
||||
const CENode = require('cenode');
|
||||
const CEModels = require('cenode/models'); // if requred
|
||||
|
||||
const node = new CENode(CEModels.core);
|
||||
```
|
||||
|
||||
(Note that for options 2 and 3, `nodejs` will need to be installed on your system).
|
||||
See the [Wiki](https://github.com/flyingsparx/CENode/wiki) for further guides and the API reference.
|
||||
|
||||
For further information, please see the Documentation section below.
|
||||
## Testing
|
||||
|
||||
## Documentation
|
||||
Clone the repository
|
||||
```
|
||||
git clone git@github.com:flyingsparx/CENode.git
|
||||
```
|
||||
|
||||
Please see the file `docs/documentation.pdf` for an overview of the CE language, CECard protocol, and CENode motivation and behaviour.
|
||||
Install the necessary dev dependencies.
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
## License
|
||||
Run tests.
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
`cenode.js` is released under the Apache License v2. See `LICENSE` for further information.
|
||||
## More Information
|
||||
|
||||
See the CENode [Wiki](https://github.com/flyingsparx/CENode/wiki) for more information, guides, and the API reference.
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
CENode is released under the Apache Licence v2. See `LICENCE` for further information.
|
||||
|
@ -1,31 +0,0 @@
|
||||
@inproceedings{ping12,
|
||||
title="Information Extraction Using Controlled English to Support Knowledge-Sharing and Decision-Making",
|
||||
author={Xue, P. and Poteet, S. and Kao, A. and Mott, D. and Braines, D. and Giammanco, C. and Pham, T.},
|
||||
booktitle = {17th ICCRTS: Operationalizing C2 Agility},
|
||||
year={2012}
|
||||
}
|
||||
|
||||
@inproceedings{preece14cs,
|
||||
author = {A. Preece and C. Gwilliams and C. Parizas and D. Pizzocaro and J. Z. Bakdash and D. Braines},
|
||||
title = "Conversational sensing",
|
||||
publisher = {SPIE},
|
||||
year = {2014},
|
||||
booktitle = {Proc Next-Generation Analyst II (SPIE Vol 9122)}
|
||||
}
|
||||
|
||||
@article{preece14hmc,
|
||||
author = {A. Preece and D. Braines and D. Pizzocaro and C. Parizas},
|
||||
year = 2014,
|
||||
title = {Human-machine conversations to support multi-agency missions},
|
||||
journal = {ACM SIGMOBILE Mobile Computing and Communications Review},
|
||||
pages = {75--84},
|
||||
volume = {18(1)}
|
||||
}
|
||||
|
||||
@misc{Mott2010,
|
||||
author = {D. Mott},
|
||||
title = {Summary of {ITA} {Controlled English}},
|
||||
howpublished = {ITA Technical paper},
|
||||
note = {\href{https://www.usukita.org/papers/5658/details.html}{https://www.usukita.org/papers/5658/details.html}},
|
||||
year = {2010}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
*{
|
||||
font-family: -apple-system, 'Helvetica', 'Sans-serif';
|
||||
font-weight:100;
|
||||
}
|
||||
|
||||
body{
|
||||
width:300px;
|
||||
margin:10px auto;
|
||||
}
|
||||
|
||||
h1{
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
textarea{
|
||||
width:96%;
|
||||
height:50px;
|
||||
border-radius:4px;
|
||||
padding:5px 2%;
|
||||
border:1px solid #007AFF;
|
||||
resize:none;
|
||||
font-size:14px;
|
||||
}
|
||||
textarea:focus{
|
||||
outline:none;
|
||||
}
|
||||
|
||||
button{
|
||||
width:100%;
|
||||
border-radius:3px;
|
||||
cursor:pointer;
|
||||
border:1px solid rgb(200,200,200);
|
||||
background:white;
|
||||
padding:5px 2%;
|
||||
}
|
||||
button:hover{
|
||||
border:1px solid rgb(100,100,100);
|
||||
}
|
||||
button:focus{
|
||||
outline:none;
|
||||
}
|
||||
|
||||
ul{
|
||||
width:100%;
|
||||
margin:10px 0px;
|
||||
padding:0px;
|
||||
}
|
||||
|
||||
li{
|
||||
list-style:none;
|
||||
display:inline-block;
|
||||
clear:both;
|
||||
margin:10px 0px;
|
||||
padding:5px;
|
||||
width:75%;
|
||||
border-radius:10px;
|
||||
background:rgb(230,230,230);
|
||||
}
|
||||
li.User{
|
||||
background:#007AFF;
|
||||
color:white;
|
||||
float:right;
|
||||
}
|
||||
li.agent1{
|
||||
float:left;
|
||||
}
|
||||
li.alert{
|
||||
background:#FF0000;
|
||||
color:white;
|
||||
float:left;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Space KB</title>
|
||||
<link href="css/styles.css" rel="stylesheet" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Space KB</h1>
|
||||
<textarea id="input"></textarea>
|
||||
<button id="send">Send message</button>
|
||||
<ul id="messages">
|
||||
<li>You can ask things like:<br>
|
||||
List instances of type planet<br>
|
||||
What is a star?<br>
|
||||
What is Jupiter?<br>
|
||||
What does Phobos orbit?<br>
|
||||
What orbits Earth?</li>
|
||||
<li>This app is based on the <a href="https://github.com/flyingsparx/CENode/blob/master/docs/getting_started.md">CENode Getting Started Guide</a>.</li>
|
||||
</ul>
|
||||
<script src="js/cenode.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,70 +0,0 @@
|
||||
var my_name = 'User';
|
||||
var PLANETS_MODEL = [
|
||||
"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 and is an imageable thing.",
|
||||
"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 planet named Mercury that orbits the star 'sun' and has 'media/Mercury.jpg' as image.",
|
||||
"there is a planet named Venus that orbits the star 'sun' and has 'media/Venus.jpg' as image.",
|
||||
"there is a planet named Earth that orbits the star 'sun' and is orbited by the moon 'the Moon' and has 'media/Earth.jpg' as image.",
|
||||
"there is a planet named Mars that orbits the star 'sun' and is orbited by the moon 'Phobos' and is orbited by the moon 'Deimos' and has 'media/Mars.jpg' as image.",
|
||||
"there is a planet named Jupiter that orbits the star 'sun' and is orbited by the moon 'Io' and is orbited by the moon 'Europa' and is orbited by the moon 'Ganymede' and is orbited by the moon 'Callisto' and has 'media/Jupiter.jpg' as image.",
|
||||
"there is a planet named Saturn that orbits the star 'sun' and is orbited by the moon 'Mimas' and is orbited by the moon 'Enceladus' and is orbited by the moon 'Tethys' and is orbited by the moon 'Dione' and is orbited by the moon 'Rhea' and is orbited by the moon 'Titan' and is orbited by the moon 'Iapetus' and has 'media/Saturn.jpg' as image.",
|
||||
"there is a planet named Uranus that orbits the star 'sun' and is orbited by the moon 'Puck' and is orbited by the moon 'Miranda' and is orbited by the moon 'Ariel' and is orbited by the moon 'Umbriel' and is orbited by the moon 'Titania' and is orbited by the moon 'Oberon' and has 'media/Uranus.jpg' as image.",
|
||||
"there is a planet named Neptune that orbits the star 'sun' and is orbited by the moon 'Triton' and is orbited by the moon 'Nereid' and is orbited by the moon 'Larissa' and has 'media/Neptune.jpg' as image.",
|
||||
];
|
||||
|
||||
var processed_cards = [];
|
||||
|
||||
var node = new CENode(MODELS.CORE, PLANETS_MODEL);
|
||||
node.agent.set_name('agent1');
|
||||
|
||||
var input = document.getElementById('input');
|
||||
var button = document.getElementById('send');
|
||||
var messages = document.getElementById('messages');
|
||||
|
||||
button.onclick = send_message;
|
||||
input.onkeyup = function(e){
|
||||
if(e.keyCode == 13){
|
||||
send_message();
|
||||
}
|
||||
};
|
||||
|
||||
function send_message(){
|
||||
var message = input.value.trim(); // CENode seems to need this
|
||||
input.value = ''; // blank the input field for new messages
|
||||
if (message == '') return; // don't submit empty messages
|
||||
var card = "there is a nl card named '{uid}' that is to the agent 'agent1' and is from the individual '"+my_name+"' and has the timestamp '{now}' as timestamp and has '"+message.replace(/'/g, "\\'")+"' as content.";
|
||||
node.add_sentence(card);
|
||||
|
||||
// Finally, prepend our message to the list of messages:
|
||||
var item = '<li class="'+my_name+'">'+message+'</li>';
|
||||
messages.innerHTML = item + messages.innerHTML;
|
||||
};
|
||||
|
||||
function poll_cards(){
|
||||
setTimeout(function(){
|
||||
var cards = node.concepts.card.all_instances; // Recursively get any cards the agent knows about
|
||||
for(var i = 0; i < cards.length; i++){
|
||||
var card = cards[i];
|
||||
if(card.is_to.name == my_name && processed_cards.indexOf(card.name) == -1){ // If sent to us and is still yet unseen
|
||||
processed_cards.push(card.name); // Add this card to the list of 'seen' cards
|
||||
var gist = card.content;
|
||||
var imgmatch = gist.match(/[\'\"](.*)[\'\"] as image/);
|
||||
var item = '<li class="'+card.is_from.name+'">'+gist;
|
||||
if(imgmatch != null) {
|
||||
item += "<br><br><img src='" + imgmatch[1] + "' width='200'>";
|
||||
}
|
||||
item += '</li>';
|
||||
messages.innerHTML = item + messages.innerHTML; // Prepend this new message to our list in the DOM
|
||||
}
|
||||
}
|
||||
poll_cards(); // Restart the method again
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
poll_cards();
|
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1,419 +0,0 @@
|
||||
\documentclass{scrartcl}
|
||||
\usepackage{framed}
|
||||
\usepackage{hyperref}
|
||||
|
||||
\newcommand{\ce}[1]{\textsf{#1}}
|
||||
\newcommand{\js}[1]{\texttt{#1}}
|
||||
|
||||
\title{\ce{cenode.js}}
|
||||
\subtitle{Revision 2.0}
|
||||
\author{W.M. Webberley \& A. Preece\footnote{CENode is a joint project between School of Computer Science \& Informatics, Cardiff University and IBM UK as part of the International Technology Alliance (ITA) in Network and Information Sciences, \href{http://www.usukitacs.com/about_ita}{www.usukitacs.com/about\_ita}.}\\ \href{http://cwenode.io}{cenode.io} $|$ \href{mailto:info@cenode.io}{info@cenode.io}}
|
||||
\date{}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\section{Preamble}
|
||||
This document describes \textit{CENode}, a JavaScript implementation of the CEStore\footnote{\href{https://www.ibm.com/developerworks/community/groups/service/html/communityview?communityUuid=558d55b6-78b6-43e6-9c14-0792481e4532}{www.ibm.com/developerworks/}} that is under development by IBM as part of the ITA project. As with the CEStore, ITA CE (Controlled English) is used `all the way down' for constructing and modifying a conceptual model, and populating it with instances.
|
||||
|
||||
CENode (and surrounding functionalities) is distributed as a single JavaScript file, known in this document as \js{cenode.js}, that is designed to work in a wide variety of settings, such as within a web app, within a JavaScript application (such as Node.js), and also as a RESTful web service. Individual devices running any type of instance of CENode are provided with equal functionality that enables users to interact with a CE-centred knowledge base at the edge of the network. The library also comes equipped with a wide range of networking capabilities that enables it to interact with known peers, subject to customisable policies, over a network connection.
|
||||
|
||||
Providing CEStore-style functionality at the network edge gives a number of key benefits;
|
||||
\begin{itemize}
|
||||
\item Users have access to and can interact with a CENode agent directly on their device. Any CE provided to the agent can be parsed locally and any local knowledge stored can later be `told' to other agents once a network connection is (re-)established.
|
||||
\item Features such as `autocorrect' and CE `spellchecking' can be provided at no bandwidth cost. The local agent can quickly check validity of any CE as it is being written in order to guide the user towards inputting correct CE and also giving insight into known concepts and instances.
|
||||
\item Instead of relying on a single CEStore server with a centralised knowledge base, CENode supports a network of peers with different ``local'' knowledge base variants.
|
||||
\end{itemize}
|
||||
|
||||
All communication with instances of CENode should be made through CE. Indeed, modifying the knowledge base requires users (or other agents) to submit information in CE, whilst extracted information can be returned programmatically in JS object format.
|
||||
|
||||
It should be noted that CENode does not aim to be a fully-fledged CE engine, in that it lacks certain capabilities that more complex systems support, such as rules. CENode is instead designed to be lightweight and easily-deployable and focuses on core CE functionality - supporting a conceptual model and instance management - and the blackboard architecture (see Section \ref{blackboard_architecture}) through the CECard conversational protocol.
|
||||
|
||||
For further information on CE and the CECard protocol, please see references~\cite{preece14hmc,preece14cs,ping12} in the bibliography.
|
||||
|
||||
The rest of this document highlights the key features of CENode and describes ways in which it can be deployed and used for a wide range of applications. Throughout the document, samples of CE are shown in \ce{this font}, while samples of JavsScript code are shown in \js{this font}.
|
||||
|
||||
\section{Supported Sentence Types}
|
||||
A formal description of the CE language is provided in~\cite{Mott2010}. CENode is designed with an aim to be fully compatible with the subset of CE covering model and instance creation, enabling interoperability between CENode and CEStore implementations. This section describes the types of CE sentences understandable by CENode, and some additional supported sentence forms.
|
||||
|
||||
Please note that CENode does not (currently) support CE rules or the `expressed by' clause used to declare synonyms.
|
||||
|
||||
\subsection{Supported ITA Controlled English Sentence Types}
|
||||
All modifications to the CENode conceptual model are made through \ce{conceptualise} statements.
|
||||
|
||||
For example, the sentence below creates a new concept, called `teacher' as a subclass of the concept `person` (assuming that `person' has already been conceptualised):\\
|
||||
\\\ce{conceptualise a $\sim$ teacher $\sim$ T that is a person.}\\
|
||||
|
||||
Since, in this example, we have declared that a teacher is a type of person, then the CENode will allow instances of teacher to be made with the properties associated with the person concept.
|
||||
|
||||
The following sentence modifies the `teacher` concept to add some further properties:\\
|
||||
\\\ce{conceptualise the teacher T $\sim$ teaches $\sim$ the class C and has the subject S as $\sim$ subject $\sim$ and has the value A as $\sim$ age $\sim$.}\\
|
||||
|
||||
Submitting the above sentences to CENode will create and modify the `teacher' concept. If `class' and `subject' are not already concepts in the model then the second sentence will fail to execute, since the node will be unable to correctly infer the types related to this concept. If the sentence is executed correctly, then the node will allow new instances of `teacher' to have a \ce{teaches} relationship and have \ce{subject} and \ce{age} values.
|
||||
|
||||
New instances of an existing concept can be declared with normal CE:\\
|
||||
\\\ce{there is a teacher named 'Mrs Smith'.}\\
|
||||
|
||||
As long as `teacher' has been declared as a concept, then our 'Mrs Smith' instance will be created. We can then modify this instance:\\
|
||||
\\\ce{the teacher 'Mrs Smith' teaches the class 'B2' and has the subject 'Computing' as subject and has '45' as age.}\\
|
||||
|
||||
In this example, CENode will attempt to do some more work on behalf of the user or agent providing this information. If, for example, the subject `Computing' had not yet been declared as an instance of \ce{subject}, then a new instance of type \ce{subject} named `Computing' will be created. The same applies for the \ce{class} `B2'. Since \ce{age} is simply a value of no particular type, there is no new instance to be created here, but the value will be embedded inside the 'Mrs Smith' object type. Supporting implicit instance creation is not as dangerous as the conceptualising equivalent, since it only involves creating an empty instance of a concept that already exists.
|
||||
|
||||
Note that if a property is encountered in the input CE that is not declared in the `teacher' conceptual model (or in any of its ancestors), then this property will be ignored. The remainder of the sentence will still be executed. As with the CEStore, instance and concept deletion is not supported.
|
||||
|
||||
\subsection{Additional Supported Sentence Types}
|
||||
CENode is also able to understand some additional sentence structures to make interaction a little easier and to support information extraction. These sentences are not CE, and are instead a form of \textit{gist} \cite{preece14hmc}. However, they can be safely sent to a CENode, which automatically processes them as if they are valid CE. In addition, whilst the CE specification defines valid sentences to be those ended in a full-stop (period), CENode will also accept sentences that do not.
|
||||
|
||||
|
||||
\subsubsection{Shorthand instance modification}
|
||||
|
||||
One key addition to the grammar is a shorthand for modifying instances. The above `teacher' example can be re-written as:\\
|
||||
\\\ce{Mrs Smith teaches the class 'B2' and has the subject 'Computing' as subject and has '45' as age.}\\
|
||||
|
||||
CENode will attempt to resolve instance names to form a valid CE sentence, which is then parsed. Note that instance names in this type of sentence do not need to be case-sensitive. Replacing `Mrs Smith' in the above example with `mrs smith' will still work.
|
||||
|
||||
In addition, CENode will process one-word instance names that are not quoted, but multi-word names and all values still need to be quoted:\\
|
||||
\\\ce{Mrs Smith teaches the class B2 and has the subject computing as subject and has '45' as age.}
|
||||
|
||||
\subsubsection{Question-asking}
|
||||
\label{questions}
|
||||
|
||||
Another addition to CENode is the ability to answer questions. This addresses the `who/what/where' information useful to researchers and also allows information to easily be extracted from the node in an easy-to-understand gist format. Note that although, technically speaking, questions in this format are themselves gist, CENode treats them as if they are valid CE, so that they can safely be embedded in \ce{ask card}s.
|
||||
|
||||
To this end, `who' and `what' questions are understood in the same way by the node. This means that the questions below are equal in meaning:\\
|
||||
\\\ce{what is mrs smith?}\\
|
||||
\ce{who is mrs smith?}\\
|
||||
|
||||
Both of these questions would result in a gist output looking something like the following:\\
|
||||
\\\ce{Mrs Smith is a teacher. Mrs Smith teaches the class 'B2' and has the subject 'Computing' as subject and has '45' as age.}\\
|
||||
|
||||
`Who' and `what' questions can also be used to find out about concepts and properties. For example, the sentence\\
|
||||
\\\ce{what is a teacher?}\\
|
||||
\\would result output similar to:\\
|
||||
\\\ce{A teacher is a type of person. An instance of teacher teaches a type of class and has a type of subject called subject and has a value called age.}\\
|
||||
|
||||
Similarly, asking\\
|
||||
\\\ce{what is teaches?}\\
|
||||
\\would give:\\
|
||||
\\\ce{'teaches' describes the relationship between a teacher and a subject (e.g. "the teacher 'TEACHER NAME' teaches the subject 'SUBJECT NAME'").}\\
|
||||
|
||||
`Where' questions work slightly differently and requires the CORE node model (see later) to be loaded to the store. `Where' questions are only valid for instances, and will only provide a response if the instance in question has a property associated with some kind of location.
|
||||
|
||||
The CORE model includes a concept called \ce{location} which can be used as a parent of other types of location (e.g. a building, a room, a road, etc.). As long as the instance in question has a property relating to any concept that has \ce{location} as an ancestor, then a meaningful response can be obtained.\\
|
||||
|
||||
For example, let's assume that the \ce{person} concept (that \ce{teacher} inherits from) supports a relationship called `lives in' that targets an instance of type \ce{house}, which is a child of \ce{location}:\\
|
||||
\\\ce{the teacher Mrs Smith lives in the house 'Number 23'.}\\
|
||||
|
||||
We can now ask a `where' question:\\
|
||||
\\\ce{Where is Mrs Smith?}\\
|
||||
\\and receive a response:\\
|
||||
\\\ce{Mrs Smith lives in the house 'Number 23'.}\\
|
||||
|
||||
Similarly, since the house `Number 23' is a location, we can ask questions like:\\
|
||||
\\\ce{what is in Number 23?} (\ce{who is in Number 23?} would also work)\\
|
||||
\\and receive a response:\\
|
||||
\\\ce{The teacher Mrs Smith lives in the house Number 23.}\\
|
||||
|
||||
In general, CENode ignores stop words and punctuation, so the following are all valid questions:\\
|
||||
\\\ce{what is an apple?}\\
|
||||
\ce{where is the banana?}\\
|
||||
\ce{Who is Mrs Smith}
|
||||
|
||||
\subsubsection{NL-parsing}
|
||||
CENode comes with a rudimentary natural language parser, which will attempt to evaluate a natural language input and return a CE string representing a guess at what the input meant.
|
||||
|
||||
This NL-parsing heavily relies on the state of the node's own KB. Therefore, the more the node knows about a particular domain in terms of instances and concepts (and the properties they have), the more accurately it will be able to understand the input.
|
||||
|
||||
|
||||
\section{Node Models}
|
||||
\label{models}
|
||||
A node's knowledge base (KB) represents the concepts and instances it knows about. Providing CE to the node updates the KB and asking questions allows the KB to be queried. Models allow a skeleton KB to be produced from which the knowledge can grow as new CE is added to the node.
|
||||
|
||||
A CE model is essentially a collection of CE sentences that can be delivered to a node in order to develop its conceptual model and populate its KB with initial instances. Since the CENode library is written in JavaScript, then a model is simply an array of CE sentences. For example, consider a simple model:
|
||||
|
||||
\begin{samepage}
|
||||
\begin{verbatim}
|
||||
var my_model = [
|
||||
"conceptualise a ~ teacher ~ T.",
|
||||
"conceptualise a ~ class ~ C.",
|
||||
"conceptualise the teacher T ~ teaches ~ the class C.",
|
||||
"there is a teacher named 'Mrs Smith' that teaches the class 'B2'."
|
||||
];
|
||||
\end{verbatim}
|
||||
\end{samepage}
|
||||
|
||||
This model can then be loaded into an instance of CENode when the node is instantiated. See Section \ref{api} for more details on this.
|
||||
|
||||
\js{cenode.js} comes bundled with models that can be used to initialise a CENode instance with some basic knowledge. As mentioned previously, loading such a model is sometimes mandatory (for example, when querying for an instance's location), since the models may include concepts and instances necessary for interacting with such information. As we progress through this document, the purpose of core models will become more clear.
|
||||
|
||||
It is usually recommended that \textit{any} instance of CENode is at least supplied the CORE model, as this includes the \ce{location} concept, as well as other concepts that are useful to subclass when further populating the model. These models are included in \js{cenode.js}'s \js{MODELS} object, so that the CORE model can be accessed by \js{MODELS.CORE}.
|
||||
|
||||
Currently, only the CORE model is recommended for general use. Instantiating a CENode with a particular model in different types of applications is described later on.
|
||||
|
||||
|
||||
|
||||
\section{CENode Agents}
|
||||
Each CENode instance is accompanied by its own agent. A node's agent is spawned upon the node's instantiation and represents the recommended interface between the node's KB and its user. In a multi-node system, agents also handle any node-node interaction through the respect of `policies' (see Section \ref{policies}).
|
||||
|
||||
A CENode agent, although bundled with \js{cenode.js}, is actually entirely separate from the node's KB, and in fact has no more access to the conceptual model than another user programmatically using the library. Agents only work properly when the CORE model has been loaded, and each agent in a given CENode system should have a unique name, which by default is `Moira' in the code. Information about an agent can be added to a node's KB (whether this refers to the local or another agent) using CE as follows (assuming that the CORE model has been loaded):\\
|
||||
\\\ce{there is an agent named 'agent1'.}
|
||||
|
||||
\subsection{Cards}
|
||||
|
||||
Agents are only useful when `cards' are used as a delivery mechanism for CE, which forms the basis of the blackboard architecture implemented by the CEStore, and which is also used as the recommended primary means for human-node and node-node communication in CENode. Different types of card extend from the \ce{card} concept, and they are all included in the CORE model. Cards wrap CE in a value property and enable the information within to be shipped to different agents as required, and a particular agent will only `open' a card to reveal the contents if the agent is an intended recipient.
|
||||
|
||||
It is rare that the \ce{card} concept is used directly. Instead, one of its subclasses should be used, since the type of card determines what the information contained represents and what the response (if any) should be. Here is an example of a \ce{tell card}:\\
|
||||
\\\ce{there is a tell card named 'msg1' that is to the agent 'agent1' and is from the agent 'agent2' and has the timestamp '123456' as timestamp and has 'there is a teacher named \textbackslash'Mrs Smith\textbackslash'' as content.}\\
|
||||
|
||||
A \ce{tell card} should be used to tell a particular agent some information, and an \ce{ask card} should be used to query for some information. Using what we've covered so far, all of the \ce{conceptualise} and instance-manipulation sentences would go into a \ce{tell card} and the questions discussed in Section 1.2.2 would be wrapped in an \ce{ask card}. Using the correct kind of card. The 'from' field of a card can be used by an agent to send back a response, if needed, and some agents may decide to ignore cards that have an old timestamp.
|
||||
|
||||
\subsubsection{Tell cards}
|
||||
|
||||
This type of card should be used to envelope valid CE. If the CE is correctly parsed by the node, then a suitable response might be returned by the agent.
|
||||
|
||||
Since tell cards are the only type of card that can act as a vehicle for CE, only the content of these cards can be used to modify the node's KB (other than to add the card instance, which would occur even if the CE content is invalid).
|
||||
|
||||
\subsubsection{Ask cards}
|
||||
|
||||
Ask cards are used to query the node's KB, and whose content must conform to one of the supported question structures (see Section \ref{questions}).
|
||||
|
||||
The agent responsible for handling this card may reply with a suitable response, or an error message if the question is invalid.
|
||||
|
||||
\subsubsection{NL cards}
|
||||
|
||||
If unsure on the type of information that is to be contained in the card, then an nl card can be used. Agents work with NL cards in the following order (a failure causes the next step to be evaluated):
|
||||
|
||||
\begin{enumerate}
|
||||
\item Test for CE-compliance: if the card's content is valid CE, the agent will automtically write a new \emph{tell card}, with the same content, from the original sender and to itself. The effect is therefore the same as directly adding a tell card containing valid CE. This process is known as \emph{autoconfirming}.
|
||||
\item Test if question: if the card contains a valid question, then the agent will write a new \emph{ask card} with the same content, from the original sender and to itself. The effect is therefore the same as directly adding an ask card containing a valid question. This process is known as \emph{autoasking}.
|
||||
\item Lastly, the content is given to the node in an attempt to understand what was meant by the card. This step tries to parse the input NL and, if successful, will result in a \emph{confirm card} in reply to the input card containing a guess at the CE best representing the input content. This card can then be confirmed by sending a tell card in reply to the confirm card with the content of the confirm card. This may then cause the node's KB to be updated.
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\subsection{Blackboard Architecture}
|
||||
\label{blackboard_architecture}
|
||||
|
||||
As mentioned, agents begin their life when the CENode they are associated with is instantiated. Agents continuously check their node's KB for any cards that are addressed to themselves. If a card is found that is addressed to and hasn't yet been seen by the agent, then the agent will act upon it.
|
||||
|
||||
If the card is a tell card, then the agent will open up the CE content contained within and feed it into its node with the aim of modifying its KB. If the card is an ask card, then the agent will attempt to answer the question and send a response back to the entity that initially sent the card.
|
||||
|
||||
If a card instance exists in a node's KB and the node's local agent is \textit{not} a recipient, then no further action will occur for this card on this node. Of course, any programs using the \js{cestore.js} library may decide to do something with it, but generally it will be ignored by the local agent (unless its name is changed to that of the intended recipient).
|
||||
|
||||
Although this may seem useless, it actually forms the basis for the blackboard architecture, in which agents and users can read and write cards from and to a node. Later on in this document we'll cover \textit{policies}, which allow agents to communicate automatically with each other in different ways. Submitting CE to agents wrapped in cards allows only the information that is actually needed by each node to be read by the agent of that node.
|
||||
|
||||
In general, any valid CE submitted to a node will be parsed immediately and the node's conceptual model appropriately updated. Sometimes, the node will return a response immediately (either programmatically or in a response to a HTTP request) containing some relevant information. This usually only occurs when the CE represents a who/what/where question. However, when submitting CE within a card envelope, no response will be returned. This is because creating instances does not invoke a response fom the node and agents work separately and asynchronously from the rest of the CENode process. Agents will read cards from their node in their own time and will write responses back to it when necessary (e.g. in the case of an `ask card' being submitted). When submitting cards, the contained CE is, essentially, parsed twice. Once when the card is initially submitted to the node (a process which involves adding an instance of `card' along with its associated information). The second time is when the agent comes round to picking cards from the node and re-submitting the contained CE directly.
|
||||
|
||||
|
||||
\section{Using CENode}
|
||||
Generally, the installation and inclusion of CENode into your project is very simple, as all that is required is an import of the \js{cenode.js} library. This section describes how this can be done more clearly.
|
||||
|
||||
\subsection{In a Web Application or Webpage}
|
||||
\label{as_a_webapp}
|
||||
In a web application or webpage, the \js{cenode.js} library can be easily imported:\\
|
||||
\\\js{<script src="cenode.js"></script>}\\
|
||||
|
||||
Once imported, a new CENode instance can be instantiated in a later \js{<script>} block and any required models can be passed as arguments. After instantiation, sentences can be added as direct CE (or embedded within cards):
|
||||
\begin{verbatim}
|
||||
<script>
|
||||
var node = new CENode(MODELS.CORE, MY_CUSTOM_MODEL);
|
||||
node.set_agent_name("agent1");
|
||||
|
||||
node.add_sentence("there is a teacher named 'Mrs Smith'");
|
||||
node.add_sentence("there is a tell card named '{uid}' that is to the agent
|
||||
'agent1' and is from the individual 'user1' and has the timestamp
|
||||
'{now}' as timestamp and has 'there is a teacher named
|
||||
\'Mrs Smith\'' as content");
|
||||
</script>
|
||||
\end{verbatim}
|
||||
|
||||
Since we have set the node's agent's name to `agent1', both of the \js{add\_sentence} lines would have equal functionality (although the node will prevent multiple instances being created with the same name and same type). In the former case, the CE will be parsed directly and the teacher will be added to the node's KB. In the latter, the card will be added to the KB, and the local agent will eventually find the card and update the KB further with the relevant information contained in the card.\\
|
||||
|
||||
Both \js{\{uid\}} and \js{\{now\}} are special character sequences that will be modified by the node once received. Please see Section \ref{api} for more information on these and for other features available to applications using the library in such a way.
|
||||
|
||||
\subsection{In a JavaScript Application}
|
||||
\label{as_an_app}
|
||||
The library is also usable as part of a Node.js program. To get started with this, you will need to first install the Node.js environment. This can be done by visiting their website to download the necessary files (https://nodejs.org) or by using an existing package manager on your system.\\
|
||||
For example, with Arch Linux:\\
|
||||
\\\js{\# pacman -S nodejs}\\
|
||||
\\with Ubuntu:\\
|
||||
\\\js{\# apt-get install nodejs}\\
|
||||
\\and with OS X (with Homebrew installed):\\
|
||||
\\\js{\$ brew install node}\\
|
||||
|
||||
Please note that the library is also mostly compatible with other JavaScript runtimes, such as \js{io.js}.
|
||||
|
||||
Once \js{Node.js} has been installed, you can create a simple Node.js app in a similar way to using the library in a web app:
|
||||
|
||||
\begin{verbatim}
|
||||
var cenode = require("./cenode.js");
|
||||
|
||||
var node = new cenode.CENode(cenode.MODELS.CORE);
|
||||
node.add_sentence(...)
|
||||
|
||||
... etc.
|
||||
\end{verbatim}
|
||||
|
||||
Beyond this point, functionality is precisely the same as that when the library is used in a web application. For more information on the programmatic API, please see Section \ref{api}.
|
||||
|
||||
|
||||
\subsection{As a RESTful Service}
|
||||
\label{as_a_service}
|
||||
|
||||
\js{cenode.js} also supports being run directly as a service using Node.js. To accomplish this, then Node.js needs to first be installed as described in the previous section. After installation, then the service can be started by running:
|
||||
|
||||
\begin{verbatim}
|
||||
$ node cenode.js
|
||||
Set local agent's name to `Moira'.
|
||||
CENode server instance running on port 5555...
|
||||
\end{verbatim}
|
||||
|
||||
By default this will start a web server on port 5555 with a local agent named `Moira'.
|
||||
|
||||
The CENode instance run in this way provides a webpage that you can use to administer the instance. To do so, visit \js{localhost:5555} in a web browser (or the hostname of the machine running the instance if not local). You will be presented with a display indicating some information about the node instance and will allow some simple controls (such as model-loading and sentence-inputs).
|
||||
|
||||
The CENode instance can be launched with different configurations by supplying command-line arguments. For example the below command will start the service on port 5432 and will set the name of the agent to `agent1' (the output from the server is included below for your information):
|
||||
|
||||
\begin{verbatim}
|
||||
$ node cenode.js agent1 5432
|
||||
Set local agent's name to `agent1'.
|
||||
CENode server instance running on port 5432...
|
||||
\end{verbatim}
|
||||
|
||||
Once running, a RESTful interface is exposed to interact with the Node. For more information on this, please see Section \ref{api}.
|
||||
|
||||
\section{Multi-Node Systems}
|
||||
As described earlier, CENode instances can either be run independently or as part of a multi-node system. This section outlines methods on how this might be accomplished. In a typical multi-node system, at least one of the nodes will need to be run as a service exposing the required HTTP endpoints.
|
||||
|
||||
\subsection{General}
|
||||
All CENode instances in a multi-node system are, by default, equal in terms of functionality and behaviour. This is the case even if each node is deployed in a different way (e.g. some nodes may be running as a service, some as a web application, and some as a programmatic JavaScript application). Providing information to (and retrieving information from) a local node is simple, as shown briefly earlier and in more detail later on, and supporting inter-node communication is also relatively easy.
|
||||
|
||||
The \js{cenode.js} library comes equipped with the ability to allow agents to communicate over the network with other agents, and will adapt automatically to the environment it exists in. For example, if running in a web page it will use the browser's \js{XMLHttpRequest} object, and if running as a Node.js app it will use Node.js's \js{http} module. Either way, there is no intervention required by users when deploying a CENode as part of a multi-node system on a variety of platforms.
|
||||
|
||||
|
||||
\subsection{Policies}
|
||||
\label{policies}
|
||||
All inter-node communication should be described by \textit{policies}. These are essentially instructions, written in CE, that instruct individual nodes to communicate with each other in different ways. All policy types understood by the agent are included in the CORE model (see Section \ref{models}).
|
||||
|
||||
Policies written to a particular CENode represent instructions that apply to its local agent. Agents periodically query the policies that are in their node's KB and act upon them accordingly. As such, policies can be created and modified using plain CE once the CENode instance is running with almost immediate effect.
|
||||
|
||||
All policies in the CORE model have an `enabled' field, and any particular policy is active as long as this field is set to `true'. For example, to disable a particular policy, named `p1', you could issue the following CE:\\
|
||||
\\\ce{the policy 'p1 has 'false' as enabled.}\\
|
||||
\\The local agent will now no longer act on this policy.
|
||||
|
||||
The rest of this Section describes the different types of policy in more detail.
|
||||
|
||||
\subsubsection{\ce{tell policy}}
|
||||
A \ce{tell policy} inherits from \ce{policy} and instructs the appropriate agent to tell the policy's target agent everything that the local agent is told.
|
||||
|
||||
For example, imagine our loocal agent is called `agent1' and we tell it about the following agent:\\
|
||||
\\\ce{there is an agent named 'agent2' that has 'agent2.address.com' as address.}\\
|
||||
\\We can now create a tell policy targeting this agent:\\
|
||||
\\\ce{there is a tell policy named 'p1' that has 'true' as enabled and has the agent 'agent2' as target.}\\
|
||||
|
||||
Once this policy has been created, then our local agent, `agent1', will tell `agent2' every piece of information that has been told to `agent1' in tell cards by wrapping the content in a new tell card and HTTP POSTing this to the appropriate endpoint at `agent2''s host address. As such, `agent2' needs to be an agent running as a service instance. Please see Section \ref{as_a_service} for instructions on setting this up.
|
||||
|
||||
Any cards which do not have `agent1' as a recipient (or any other type of card) will not be included as part of the policy.
|
||||
|
||||
\subsubsection{\ce{ask policy}}
|
||||
An \ce{ask policy} works in almost exactly the same way as a tell policy (with our local agent named `agent1'):\\
|
||||
\\\ce{there is an ask policy named 'p1' that has 'true' as enabled and has the agent 'agent2' as target.}\\
|
||||
|
||||
In this scenario, every \ce{ask card} sent to `agent1' will also be sent to `agent2' using a HTTP POST request. As with targets of a \ce{tell policy}, target agents of an \ce{ask policy} must be instances running as a service instance.
|
||||
|
||||
Ask policies are mostly useless unless the agent acting on the policy is able to receive a response from the policy's target. As discussed in Section \ref{blackboard_architecture}, communication between agents and individuals using cards is \textit{asynchronous}, and therefore an answer to a question cannot be included in the response of the POST request made as a result of the policy. In reality, when an `ask card' is POSTed to the target, its agent will get round to reading the card in its own time and will write a card back to its \textit{own} store if the card requires a reply.
|
||||
|
||||
Therefore, most multi-node setups using an `ask policy' will also involve a `listen policy' targeting the same target as the `ask policy'. See Section \ref{listen_policy} for more information.
|
||||
|
||||
\subsubsection{\ce{listen policy}}
|
||||
\label{listen_policy}
|
||||
A \ce{listen policy} instructs the local agent, `agent1', to periodically poll the target agent for instances of `tell card' sent to `agent1'. Any cards found are opened and the content is added to the agent's node's KB as normal.\\
|
||||
|
||||
As with the previous two policy types, any target agent must be in a node running a as a service instance.\\
|
||||
|
||||
Listen policies are useful in conjunction with ask policies, since they enable a response to be retrieved from the target of the ask policy. For example, consider the following setup (assuming the local agent is named `agent1'):\\
|
||||
\\\ce{there is an agent named 'agent2' that has 'agent2.com' as address.\\
|
||||
there is an ask policy named 'p1' that has 'true' as enabled and has the agent 'agent2' as target.\\
|
||||
there is a listen policy named 'p2' that has 'true' as enabled and has the agent 'agent2' as target.}\\
|
||||
|
||||
This setup will cause `agent1' to forward all ask cards it receives to `agent2' and will be able to receive a response from `agent2', through the listen policy, once `agent2' has read and replied to the ask card.
|
||||
|
||||
\subsubsection{\ce{forwardall policy}}
|
||||
A \ce{forwardall policy} is slightly more complex because it has more options in its configuration. The general principle is that the agent the policy is active on will forward some tell cards that have been sent to this agent on to a set of other agents as required. Unlike the other policy types, a \ce{forwardall policy} does not trigger any network requests. Instead, any card-forwardings are made simply by adding targets as \textit{recipients} of the cards. These can then be retrieved by other agents who have a \ce{listen policy} targeting this node.
|
||||
|
||||
As with the previous examples, imagine the local agent which is acting on the \ce{forwardall policy} is named `agent1'.
|
||||
|
||||
The construction of a \ce{forwardall policy} might look like this:\\
|
||||
\\\ce{there is a forwardall policy named 'p1' that has 'true' as enabled and has the timestamp '0' as start time and has 'true' as all agents.}\\
|
||||
|
||||
In the above example, any tell cards that have previously been sent to `agent1' and any arriving in future whilst the policy is enabled will have every agent known by agent1's node added as a recipient. Then, if any of these agents make a request to this node (as a result of a \ce{listen policy} or otherwise), they can access these cards.
|
||||
|
||||
A node can discover other agents in two primary ways. One is explicit, in that the node has been given CE to describe a new agent:\\
|
||||
\\\ce{there is an agent named 'agent2'.}\\
|
||||
|
||||
The other is implicit, where the node will add to its KB any unknown instances that are mentioned. For example, assume the node does not yet have the agent `agent2' in its KB and then receives the following card:\\
|
||||
\\\ce{there is a tell card that is to the agent 'agent1' and is from the agent 'agent2' and has 'there is a teacher named \'Mrs Smith\'' as content.}\\
|
||||
|
||||
In this case, the node will automatically create an instance of agent named `agent2', thus discovering its existence.
|
||||
|
||||
The `start time' field specifies that the policy should only affect cards with a timestamp greater than this, and so this can be set to `0' to activate the policy for all tell cards sent to `agent1' during its lifetime. The `all agents' field is a boolean which, if `true', specifies that \textit{all} known agents should be added as a recipient.
|
||||
|
||||
If `all agents' is set to `false' instead, then a set of agent recipients can be specified. Consider the more complex example below:\\
|
||||
\\\ce{there is a forwardall policy named 'p2' that has 'true' as enabled and has the timestamp '12345' as start time and has the agent 'agent2' as target and has the agent 'agent3' as target}\\
|
||||
|
||||
In the above example, the policy will cause the agent `agent1' to add both `agent2' and `agent3' as recipients to all tell cards sent to `agent1' with a timestamp greater than `12345' from now until the policy is disabled.
|
||||
|
||||
\subsubsection{\ce{feedback policy}}
|
||||
A \ce{feedback policy} can be applied to an agent in order to make it give some kind of feedback to the agent or individual that has submitted a `tell card' to it. This behaviour might be useful for providing information on input submitted to the node, and allows the local agent to report any misunderstandings in the input CE.
|
||||
|
||||
A \ce{feedback policy} follows a similar setup to the other policy types, in that it can be enabled and can target a particular agent or individual, but, like the \ce{forwardall policy} it will \textit{not} invoke a network request. Instead, any feedback is included in a \ce{tell card} addressed to the target, which is written to the agent's own node. Thus, if responses are required over the network, a \ce{listen policy} must also be used.
|
||||
|
||||
Since no network activity is directly involved (unless there is a \ce{listen policy} in place), this type of policy is mostly useful for JavaScript or web applications using the \js{cenode.js} library directly. Imagine that the local agent is named `agent1' and there is a user, known as the individual `individual1', that is submitting information to the node's agent through \ce{tell card}s:\\
|
||||
\\\ce{there is a feedback policy named 'p1' that has 'true' as enabled and has the individual 'individual1' as target and has 'full' as acknowledgement.}\\
|
||||
|
||||
With this policy in place, `agent1' will respond to all \ce{tell card}s sent from `individual1' with a full description of the action taken by `agent1' on the node. If this is an error message, then the node will attempt to include information on which parts of the input sentence were not understood. If the message was understood fully, then the full understood CE will be returned in the response.
|
||||
|
||||
For security, it may sometimes be necessary for nodes to be restricted on the information returned. For example, in order to keep the inner knowledge of the node obfuscated for whatever reason, the \ce{acknowledgement} property of the policy can be set to `basic'. In this scenario, only an `OK' will be sent back to the agent or individual that submitted the original tell card, with no indication of the inner knowledge of the node.
|
||||
|
||||
To keep agents from giving any feedback whatsoever, then simply disable the policy or don't set the policy in the first place.
|
||||
|
||||
\subsection{Example Network Topologies Using Policies}
|
||||
Using policies allows for a wide variety of possible network topologies. Combining policies allow for useful configurations of multi-node setups. This section outlines a couple of examples for inspiration.
|
||||
|
||||
\subsubsection{`Point-to-point topology'}
|
||||
In this example, two CENode instances communicate directly to each other by telling each other everything.
|
||||
|
||||
To implement this, two instances of CENode (each with a different names) running as services need to be launched. Each instance needs to know the address of the other instance's agent and a tell policy is needed on each node.
|
||||
|
||||
For example, consider `agent1' runs on `agent1.com' and `agent2' runs on `agent2.com'. The configuration CE can be added on each instance's webpage control panel (see Section \ref{as_a_service} for more information).
|
||||
|
||||
On agent1's node, the following sentences are required:\\
|
||||
\\\ce{there is an agent named 'agent2' that has 'agent2.com' as address.\\
|
||||
there is a tell policy named 'p1' that has 'true' as enabled and has the agent 'agent2' as target.}\\
|
||||
|
||||
Agent2's setup is symmetrical:\\
|
||||
\\\ce{there is an agent named 'agent1' that has 'agent1.com' as address.\\
|
||||
there is a tell policy named 'p1' that has 'true' as enabled and has the agent 'agent1' as target.}\\
|
||||
|
||||
|
||||
\subsubsection{`Star topology'}
|
||||
In this example, one CENode instance, at the centre of the star, acts as a router of information between any number of `client' nodes. The router node needs to be run as a service, but the clients can be run in any configuration. In this scenario, each node will tell the router everything it knows, and the router will forward this information on to every other client node.
|
||||
|
||||
Firstly, each client node needs to know about the router node and to tell it everything and listen for any cards the router node might have for it:\\
|
||||
\\\ce{there is an agent named 'router' that has 'router.com' as address.\\
|
||||
there is a tell policy named 'p1' that has 'true' as enabled and has the agent 'router' as target.\\
|
||||
there is a listen policy named 'p2' that has 'true' as enabled and has the agent 'router' as target.}\\
|
||||
|
||||
Secondly, the router node needs to simply forward every message it receives on to every agent it knows about:\\
|
||||
\\\ce{there is a forwardall policy named 'p1' that has 'true' as enabled and has the timestamp '0' as start time and has 'true' as all agents.}
|
||||
|
||||
|
||||
\section{CENode API}
|
||||
\label{api}
|
||||
\begin{emph}
|
||||
This section is now deprecated and has been removed. Instead, please see the separate API document outlining the APIs supported by CENode. This can be found in the root of CENode's home repository at github.com/flyingsparx/CENode.
|
||||
\end{emph}
|
||||
|
||||
\bibliographystyle{plain}
|
||||
\bibliography{citations}
|
||||
|
||||
\end{document}
|
@ -1,295 +0,0 @@
|
||||
# Getting started with CENode
|
||||
|
||||
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](http://cenode.io/documentation.pdf) outlines key use-cases and gives an overview of the APIs exposed by the library.
|
||||
|
||||
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.
|
||||
|
||||
## Companion project
|
||||
|
||||
The code created as a result of this guide has been put together into a complete project that can be used to help illustrate the steps descreibed below.
|
||||
|
||||
The companion project does not really go beyond the instructions in this guide, aside from adding some simple styles.
|
||||
|
||||
The project can be accessed from its [home repository](https://github.com/flyingsparx/CENode-chat).
|
||||
|
||||
|
||||
## Setting up the environment
|
||||
|
||||
Create a new directory in your normal project space and change into it.
|
||||
|
||||
```bash
|
||||
$ mkdir ~/Project/MyCENodeProject
|
||||
$ cd ~/Project/MyCENodeProject
|
||||
```
|
||||
|
||||
We 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:
|
||||
|
||||
```bash
|
||||
$ mkdir js
|
||||
$ touch index.html
|
||||
```
|
||||
|
||||
|
||||
## Setting up version control
|
||||
|
||||
If you would like to place this project under Git version control, then initialise the repository:
|
||||
```bash
|
||||
$ git init
|
||||
```
|
||||
|
||||
To make an initial commit, then first ensure your global Git settings have been properly set:
|
||||
```bash
|
||||
$ 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:
|
||||
```bash
|
||||
$ 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](https://help.github.com/articles/adding-a-remote) on how to do this with GitHub.
|
||||
|
||||
|
||||
## Installing dependencies
|
||||
|
||||
The only dependency our app has (unless you later require more) is `cenode.js` itself.
|
||||
|
||||
`cenode.js` can be obtained directly from its website at [cenode.io/cenode.js](http://cenode.io/cenode.js). You can use CURL to do this for you:
|
||||
```bash
|
||||
$ curl -o js/cenode.js http://cenode.io/cenode.js
|
||||
```
|
||||
|
||||
## Building out the app
|
||||
|
||||
We will write all of our app logic within a file named `main.js`, so create this:
|
||||
```bash
|
||||
$ touch js/main.js
|
||||
```
|
||||
|
||||
Create the skeleton of the app by ediing `index.html`:
|
||||
#### `index.html`
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My CENode app</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>My app</h1>
|
||||
<script src="js/cenode.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.js` library. Start by initialising the library with some core data (see [CENode models](https://github.com/flyingsparx/CENode/blob/master/docs/getting_started.md#cenode-models) below) and setting the node's local agent name:
|
||||
|
||||
#### `main.js`
|
||||
```javascript
|
||||
var node = new CENode(MODELS.CORE);
|
||||
node.agent.set_name('agent1');
|
||||
```
|
||||
|
||||
This code creates an instance of CENode, which in turn spins up a CEAgent, which continuously runs in the background and is able to respond to certain events, as we'll cover later.
|
||||
|
||||
### CENode models
|
||||
|
||||
Notice that we have passed a variable `MODELS.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:
|
||||
```javascript
|
||||
var my_model = [
|
||||
"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.:
|
||||
```javascript
|
||||
var node = new CENode(MODELS.CORE, my_model);
|
||||
```
|
||||
|
||||
_(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.)_
|
||||
|
||||
In this guide, we don't _need_ to prepopulate the node if you don't want to, but it might give you and the agent a bit more to talk about if you do.
|
||||
|
||||
|
||||
### Building the messaging interface
|
||||
|
||||
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>`:
|
||||
#### `index.html`
|
||||
```html
|
||||
<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.
|
||||
#### `main.js`
|
||||
```javascript
|
||||
var my_name = 'User';
|
||||
|
||||
var input = document.getElementById('input');
|
||||
var button = document.getElementById('send');
|
||||
var messages = document.getElementById('messages');
|
||||
|
||||
button.onclick = function(){
|
||||
var message = input.value;
|
||||
input.value = ''; // blank the input field for new messages
|
||||
var card = "there is a nl card named '{uid}' that is to the agent 'agent1' and is from the individual '"+my_name+"' and has the timestamp '{now}' as timestamp and has '"+message.replace(/'/g, "\\'")+"' as content.";
|
||||
node.add_sentence(card);
|
||||
|
||||
// Finally, prepend our message to the list of messages:
|
||||
var 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:
|
||||
```javascript
|
||||
var my_model = [
|
||||
...
|
||||
"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 intialising 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:
|
||||
#### `main.js`
|
||||
```javascript
|
||||
var processed_cards = []; // A list of cards we've already seen and don't need to process again
|
||||
|
||||
function poll_cards(){
|
||||
setTimeout(function(){
|
||||
var cards = node.get_instances('card', true); // Recursively get any cards the agent knows about
|
||||
for(var i = 0; i < cards.length; i++){
|
||||
var card = cards[i];
|
||||
if(card.is_to.name == my_name && processed_cards.indexOf(card.name) == -1){ // If sent to us and is still yet unseen
|
||||
processed_cards.push(card.name); // Add this card to the list of 'seen' cards
|
||||
var item = '<li>'+card.content+'</li>';
|
||||
messages.innerHTML = item + messages.innerHTML; // Prepend this new message to our list in the DOM
|
||||
}
|
||||
}
|
||||
poll_cards(); // 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 `poll_cards()` repeating function is called when the app starts:
|
||||
|
||||
|
||||
#### `main.js`
|
||||
```javascript
|
||||
poll_cards();
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Notes on instances
|
||||
|
||||
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:
|
||||
```javascript
|
||||
console.log(card);
|
||||
```
|
||||
|
||||
### Direct properties
|
||||
|
||||
Properties such as `name` can be accessed directly, e.g.:
|
||||
```javascript
|
||||
var 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?
|
||||
```
|
||||
|
||||
### Values
|
||||
|
||||
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):
|
||||
```javascript
|
||||
var value = instance.label;
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```javascript
|
||||
var 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).
|
||||
|
||||
### Relationships
|
||||
|
||||
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'):
|
||||
```javascript
|
||||
var rel = instance.label;
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```javascript
|
||||
var 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.
|
||||
|
||||
|
||||
## Taking it further
|
||||
|
||||
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).
|
||||
|
||||
Also, the CENode has a 'autocomplete' feature that will try to guess the next word/phrase in the sentence based on your current input. Read the docs to check out the `guess_next()` function for this and try to find a way to include it in the code.
|
||||
|
||||
Remember to checkout the [companion project](https://github.com/flyingsparx/CENode-chat) for a complete implementation of the code covered in this guide.
|
344
docs/index.html
@ -1,344 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=500, user-scalable=0">
|
||||
<title>CENode</title>
|
||||
<link href='https://fonts.googleapis.com/css?family=PT+Sans' rel='stylesheet' type='text/css'>
|
||||
<link href='https://fonts.googleapis.com/css?family=Droid+Serif' rel='stylesheet' type='text/css'>
|
||||
<style>
|
||||
body,html{
|
||||
margin:0px;
|
||||
padding:0px;
|
||||
}
|
||||
body{
|
||||
background:rgb(246, 246, 244);
|
||||
font-family:'PT Sans';
|
||||
}
|
||||
|
||||
header{
|
||||
background: rgb(114, 191, 185);
|
||||
/*background: rgb(191, 128, 114);*/
|
||||
height:150px;
|
||||
box-shadow:0px 0px 10px gray;
|
||||
position:relative;
|
||||
padding-top:40px;
|
||||
margin-bottom:40px;
|
||||
}
|
||||
header h1{
|
||||
color:white;
|
||||
font-family:'Droid Serif';
|
||||
margin:0px auto 0px 50px;
|
||||
font-weight:100;
|
||||
font-size:50px;
|
||||
letter-spacing:3px;
|
||||
}
|
||||
header h2.tagline{
|
||||
font-size:20px;
|
||||
color:white;
|
||||
margin-left:50px;
|
||||
margin-top:0px;
|
||||
font-weight:100;
|
||||
}
|
||||
header nav{
|
||||
position:absolute;
|
||||
top:10px;
|
||||
right:10px;
|
||||
}
|
||||
header nav a{
|
||||
display:inline-block;
|
||||
margin:0px 10px;
|
||||
}
|
||||
header nav a span{
|
||||
width:40px;
|
||||
height:40px;
|
||||
display:block;
|
||||
background-size:cover;
|
||||
}
|
||||
header nav a span.github{
|
||||
background-image:url('index_media/github-white.png');
|
||||
}
|
||||
header nav a span.github:hover{
|
||||
background-image:url('index_media/github-white-filled.png');
|
||||
}
|
||||
header nav a span.docs{
|
||||
background-image:url('index_media/paper-white.png');
|
||||
}
|
||||
header nav a span.docs:hover{
|
||||
background-image:url('index_media/paper-white-filled.png');
|
||||
}
|
||||
|
||||
footer{
|
||||
background:#444;
|
||||
margin-top:50px;
|
||||
padding-top:40px;
|
||||
display:block;
|
||||
min-height:20px;
|
||||
box-shadow:0px 0px 10px gray;
|
||||
}
|
||||
footer *{
|
||||
color:white;
|
||||
}
|
||||
footer img.attribution{
|
||||
margin:10px;
|
||||
width:130px;
|
||||
display:inline-block;
|
||||
vertical-align:text-top;
|
||||
}
|
||||
footer div.attribution{
|
||||
display:inline-block;
|
||||
max-width:500px;
|
||||
vertical-align:text-top;
|
||||
margin:10px;
|
||||
}
|
||||
footer a:link{color:rgb(114, 191, 185);}
|
||||
footer a:visited{color:rgb(114, 191, 185);}
|
||||
footer a:active{coor:white;}
|
||||
|
||||
|
||||
h2{
|
||||
color:rgb(150,150,150);
|
||||
font-weight:100;
|
||||
}
|
||||
h3{
|
||||
font-weight:100;
|
||||
}
|
||||
|
||||
p.justified{
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
}
|
||||
|
||||
section{
|
||||
display:block;
|
||||
min-height:100px;
|
||||
margin:20px auto;
|
||||
}
|
||||
section h2{
|
||||
text-align:center;
|
||||
|
||||
}
|
||||
.inline{
|
||||
display:inline-block;
|
||||
margin: 10px 10px;
|
||||
}
|
||||
.centre{
|
||||
text-align:center;
|
||||
}
|
||||
.left{
|
||||
text-align:left;
|
||||
}
|
||||
.box{
|
||||
position:relative;
|
||||
width:200px;
|
||||
padding:10px;
|
||||
padding-left:10px;
|
||||
padding-right:10px;
|
||||
border-radius:2px;
|
||||
background-color:rgb(114, 191, 185);
|
||||
color:white;
|
||||
}
|
||||
blockquote{
|
||||
padding-top:35px !important;
|
||||
background-image:url('index_media/quote-white.png');
|
||||
background-position:8px 8px;
|
||||
background-repeat:no-repeat;
|
||||
background-size:25px 25px;
|
||||
}
|
||||
.box.primary{
|
||||
/*background-color:rgb(114, 191, 185);*/
|
||||
|
||||
background-color: rgb(191, 128, 114);
|
||||
color:white;
|
||||
min-height:320px;
|
||||
min-width:200px;
|
||||
max-width:400px;
|
||||
width:25%;
|
||||
vertical-align:text-top;
|
||||
}
|
||||
.box img.main{
|
||||
display:block;
|
||||
margin:10px auto;
|
||||
height:60px;
|
||||
}
|
||||
.main_buttons{
|
||||
margin-top:20px;
|
||||
padding-top:10px;
|
||||
border-top:1px dashed rgb(210,210,210);
|
||||
}
|
||||
p.tagline span{
|
||||
display:inline-block;
|
||||
margin:2px 5px;
|
||||
}
|
||||
p.tagline span:first-child{
|
||||
padding-right:10px;
|
||||
border-right:1px solid rgb(150,150,150);
|
||||
}
|
||||
img.info{
|
||||
opacity:0.6;
|
||||
width:80px;
|
||||
}
|
||||
|
||||
a.button{
|
||||
display:inline-block;
|
||||
background-color:rgb(137, 191, 114);
|
||||
opacity:0.85;
|
||||
color:white;
|
||||
text-transform:uppercase;
|
||||
padding:10px;
|
||||
border:none;
|
||||
font-size:20px;
|
||||
border-bottom:5px solid rgb(200,200,200);
|
||||
text-decoration:none;
|
||||
border-radius:2px;
|
||||
}
|
||||
a.button:hover{
|
||||
opacity:1.0;
|
||||
}
|
||||
a.button.primary{
|
||||
background-color:rgb(114, 191, 185);
|
||||
}
|
||||
a.button.icon{
|
||||
padding-left:30px;
|
||||
background-position:2px center;
|
||||
background-size:23px 23px;
|
||||
background-repeat:no-repeat;
|
||||
}
|
||||
a.button.docs{
|
||||
background-image:url('index_media/paper-white-filled.png');
|
||||
}
|
||||
a.button.download{
|
||||
background-image:url('index_media/download-white.png');
|
||||
}
|
||||
a.button.learn{
|
||||
background-image:url('index_media/chevron-right.png');
|
||||
}
|
||||
a.button.try{
|
||||
background-image:url('index_media/play-white.png');
|
||||
}
|
||||
|
||||
a.button.inbox{
|
||||
background-color:white;
|
||||
color:rgb(100,100,100);
|
||||
position:absolute;
|
||||
bottom:10px;
|
||||
right:2%;
|
||||
font-size:15px;
|
||||
opacity:0.95;
|
||||
}
|
||||
</style>
|
||||
<style media="(max-width: 800px),handheld">
|
||||
html,body{
|
||||
margin:0px;padding:0px;
|
||||
}
|
||||
header{
|
||||
height:auto;
|
||||
padding-top:10px;
|
||||
}
|
||||
header h1{
|
||||
padding:0px;
|
||||
margin:0px;
|
||||
text-align:center;
|
||||
}
|
||||
header h2.tagline{
|
||||
font-size:13px;
|
||||
text-align:center;
|
||||
margin:0px 0px 20px 0px;
|
||||
}
|
||||
header nav{
|
||||
position:relative;
|
||||
text-align:center;
|
||||
width:100%;
|
||||
display:block;
|
||||
clear:both;
|
||||
margin-bottom:5px;
|
||||
height:50px;
|
||||
top:0px;
|
||||
right:0px;
|
||||
}
|
||||
header nav a img{
|
||||
width:40px;
|
||||
}
|
||||
img.info{
|
||||
width:20%;
|
||||
margin:1%;
|
||||
}
|
||||
blockquote.box{
|
||||
width:40%;
|
||||
margin:1%;
|
||||
}
|
||||
.box.primary{
|
||||
width:96%;
|
||||
min-width:96%;
|
||||
max-width:96%;
|
||||
padding:10px 1%;
|
||||
margin:10px 1%;
|
||||
min-height:0px;
|
||||
}
|
||||
.box.primary a.button.inbox{
|
||||
position:relative;
|
||||
margin-top:30px;
|
||||
left:20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>CENode.js</h1>
|
||||
<h2 class="tagline">Enabling human-machine conversations at the network edge</h2>
|
||||
<nav>
|
||||
<a href="https://github.com/flyingsparx/CENode" target="_blank"><span class="github"></span></a>
|
||||
<a href="documentation.pdf" target="_blank"><span class="docs"></span></a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="overview centre">
|
||||
<div>
|
||||
<img src="index_media/person.png" class="inline info"/>
|
||||
<blockquote class="inline box">
|
||||
<p>there is a person named 'John Smith' that is a doctor and is located in the city 'Cardiff'.</p>
|
||||
</blockquote>
|
||||
<img src="index_media/gear.png" class="inline info"/>
|
||||
</div>
|
||||
|
||||
<p class="tagline"><span>Low complexity</span><span>No ambiguity</span></p>
|
||||
|
||||
<div class="main_buttons">
|
||||
<a class="button inline icon docs" href="intro.pdf" target="_blank">Intro</a>
|
||||
<a class="button inline icon try" href="demo/" target="_blank">Try it</a>
|
||||
<a class="button inline icon docs" href="documentation.pdf" target="_blank">Docs</a>
|
||||
<a class="button primary inline icon download" href="/cenode.js" download>Download</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="overview centre">
|
||||
<div class="primary box inline left">
|
||||
<img src="index_media/text-white.png" class="main" />
|
||||
<h3>Controlled English</h3>
|
||||
<p>CE is an information representation designed to be easily processable by a machine while also being readable and writable by humans.</p>
|
||||
<a class="button inbox icon learn" target="_blank" href="https://www.ibm.com/developerworks/community/groups/service/html/communityview?communityUuid=558d55b6-78b6-43e6-9c14-0792481e4532]">Learn more</a>
|
||||
</div>
|
||||
<div class="primary box inline left">
|
||||
<img src="index_media/chatboxes-white.png" class="main" />
|
||||
<h3>CECards</h3>
|
||||
<p>CECards enable conversations that flow from natural language to CE and back again through an exchange of messages based on speech-act theory.</p>
|
||||
<a class="button inbox icon learn" target="_blank" href="http://orca.cf.ac.uk/57509/">Learn more</a>
|
||||
</div>
|
||||
<div class="primary box inline left">
|
||||
<img src="index_media/iphone-white.png" class="main" />
|
||||
<h3>Moira</h3>
|
||||
<p>Moira (Mobile Information Reporting App) is a conversational agent designed to mediate interactions between human users and machine agents, using CE. </p>
|
||||
<a class="button inbox icon learn" target="_blank" href="http://arxiv.org/abs/1406.1907">Learn more</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<img src="index_media/cardiff_logo.jpg" class="attribution" />
|
||||
<img src="index_media/ita_logo.png" class="attribution" />
|
||||
<div class="attribution">
|
||||
<p>CENode is a collaboration between <a href="http://cs.cf.ac.uk" target="_blank">Cardiff University School of Computer Science & Informatics</a> and <a href="http://www.ibm.com/uk/en" target="_blank">IBM UK</a> as part of the <a href="https://www.usukitacs.com/about_ita_Int" target="_blank">International Technology Alliance in Network & Information Sciences</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 6.8 KiB |
BIN
docs/intro.pdf
@ -1,110 +0,0 @@
|
||||
# Updating to cenode.js v2.
|
||||
|
||||
## Migration and update nodes
|
||||
|
||||
CENode v2. comes with a series of enhancements that should make programming with the library a bit easier.
|
||||
|
||||
The library is now more modular, in the sense that a lot of the work the CENode class used to do is now spread out amongst the new CEInstance and CEConcept classes. For more information on these, please see the API docs in the root of this repository.
|
||||
|
||||
### New features
|
||||
|
||||
This version introduces the following new features:
|
||||
|
||||
#### Helpers
|
||||
|
||||
CENode v2 ships with a very large number of new properties that should make programming with the library more intuitive. The following examples demonstrate some of the features behind this update, and also demonstrate the easier use of the CEConept and CEInstance classes. The examples assume a `CENode` instance (called `node`) has already been instantiated.
|
||||
|
||||
```javascript
|
||||
node.agent // the CEAgent of this node
|
||||
|
||||
node.instances.mrs_smith // the CEInstance representing Mrs Smith
|
||||
node.instances.mrs_smith.teaches // the last-reported CEInstance of the class Mrs Smith teaches
|
||||
node.instances.card_1.is_to // the last-reported CEInstance of the agent/user Card 1 is sent to
|
||||
node.instances.card_1.is_tos // an array of recipients of Card 1
|
||||
node.instances.mrs_smith.synonyms // alternative names for Mrs Smith
|
||||
node.instances.mrs_smith.age // the last-reported age of Mrs Smith
|
||||
node.instances.mrs_smith.ages // array of all reported ages of Mrs Smith
|
||||
node.instances.mrs_smith.type // CEConcept of the teacher concept
|
||||
node.instances.mrs_smith.type.ancestors // CEConcept array of ancestors of teacher concept
|
||||
node.instances.mrs_smith.type.parents[0].all_instances // CEInstance array of any type of the first parent of the teacher concept
|
||||
|
||||
node.concepts.teacher // CEConcept representing a teacher
|
||||
node.concepts.teacher.instances // list of instances of type teacher
|
||||
var teacher_concept = node.concepts.teacher // assign this CEConcept
|
||||
teacher_concept.instances.mrs_smith.name // Mrs Smith
|
||||
teacher_concept.teaches // CEConcept representing a class
|
||||
...
|
||||
```
|
||||
|
||||
The list goes on, and the chaining capability (as demonstrated) can be quite powerful for querying the KB. The API documentation lists most of the available helpers, if you'd like further information.
|
||||
|
||||
#### Synonyms
|
||||
|
||||
See the relevant [ticket](https://github.com/flyingsparx/CENode/issues/10).
|
||||
|
||||
CENode v2 supports the assignment of synonyms to concepts and instances. These are defined in CE for both concepts and instances in a similar way.
|
||||
|
||||
For concepts:
|
||||
|
||||
`conceptualise the teacher T ~ is expressed by ~ 'instructor' and is ...`
|
||||
|
||||
And for instances:
|
||||
|
||||
`there is a teacher named 'Mrs Smith' that is expressed by 'Jane Smith' and is expressed by 'J Smith'`
|
||||
|
||||
Once defined, synonyms can be used interchangeably:
|
||||
|
||||
`the instructor 'Jane Smith' teaches the ...`
|
||||
|
||||
_Please note_: synonyms must be defined in valid CE, and the node's NL-parser is not (currently) equipped to handle the 'expressed by' clause.
|
||||
|
||||
#### Synchronous card-handling
|
||||
|
||||
See the relevant [ticket](https://github.com/flyingsparx/CENode/issues/12).
|
||||
|
||||
Users of the library writing for more lightweight systems may not have access to key requirements of the library, such as the `window.setTimeout()` function. To help with this problem, CENode now allows developers to synchronously process cards with the agent, using the `agent.handle_card(CEInstance)` function.
|
||||
|
||||
Please see the API documentation for further information on this.
|
||||
|
||||
#### General
|
||||
|
||||
The new modular approach to the library will allow for much easier adaption and growth, allowing new features to be added. By clearly segregating concept objects from instance objects (and from all other objects), the library becomes more comprehensible and easier to develop with.
|
||||
|
||||
### Migration guide
|
||||
|
||||
Although version 2 brings in a lot of new features, it is generally not backwards-compatible with apps that used version 1 of the library.
|
||||
|
||||
This section aims to demonstrate how best to replace v1-compatible code so that your app can work with v2. Each subsection is titled by the feature that is no longer available in v2 and complemented by a v2 workaround.
|
||||
|
||||
#### `node.get_instance_type(instance)`
|
||||
Previously this method returned a string referring to the type of the input instance object. For example, if the instance object is a tell card, then this function would just return 'tell card'.
|
||||
|
||||
Instead, the type of the CEInstance can now be directly accessed with `instance.type`, so to get the same result as the deprecated function, one would use `instance.type.name`.
|
||||
|
||||
#### `node.get_instance_ce(instance)`
|
||||
Similarly, this function's effect can now be achieved by accessing a property on the CEInstance directly: `instance.ce`.
|
||||
|
||||
#### `node.get_instance_gist(instance)`
|
||||
Similarly, this function's effect can now be achieved by accessing a property on the CEInstance directly: `instance.gist`.
|
||||
|
||||
#### `node.get_concept_ce(concept)`
|
||||
As above: `concept.ce`.
|
||||
|
||||
#### `node.get_concept_gist(concept)`
|
||||
As above: `concept.gist`.
|
||||
|
||||
#### `node.set_agent_name(name)`
|
||||
Now, onw should access the agent through the node, and update the name directly: `node.agent.set_name(name)`.
|
||||
|
||||
#### `node.get_agent_name()`
|
||||
Similarly, access through the CENode instance: `node.agent.get_name()`.
|
||||
|
||||
#### `node.get_instance_value(instance, value_name)` and `node.get_instance_relationship(instance, rel_name)`
|
||||
It's now possible to achieve the same effect by accessing the property directly. For example, to access the timestamp of a card CEInstance: `card.timestamp` and to get the latest recipient: `card.is_to`.
|
||||
|
||||
In these cases, simply lower-case the property name, and replace spaces with underscores. However, if you like to be able to access the value or relationship using the precise label, then you can use `card.property('timestamp')` or `card.property('is to')`.
|
||||
|
||||
#### `node.get_instance_values(instance, value_name)` and `node.get_instance_relationships(instance, rel_name)`
|
||||
Similarly, access through the plural form of the property to get all reported properties of this name: `card.timestamps` and `card.is_tos` (i.e. simply add an 's' onto the end of the property).
|
||||
|
||||
As before, you can also use `card.properties('timestamp')` or `card.properties('is to')`. The latter method might be useful if you assign property names with "s"s at the end (e.g. if you want a property called 'sms' and a different one called 'smss').
|
@ -1,78 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<div style="position:relative; height:100px;">
|
||||
<textarea style = "top:0px;left:0px;position:absolute;width:100%;max-width:100%;;margin:10px auto;z-index:2;height:50px;background:none;" id="input"></textarea>
|
||||
<textarea style = "top:0px;left:0px;position:absolute;width:100%;max-width:100%;;margin:10px auto;z-index:1;height:50px;color:rgb(150,150,150);" id="guess"></textarea>
|
||||
</div>
|
||||
|
||||
<p style="font-size:10px;font-family:'Courier', 'Monospace';" id="card"></p>
|
||||
<br />
|
||||
|
||||
<button id="send2">Submit</button>
|
||||
<button id="send">Submit as card</button>
|
||||
<hr />
|
||||
<h3>Internal representation of CENode</h3>
|
||||
<div style="width:48%;float:left;margin-bottom:20px;">
|
||||
<p>Instances:</p>
|
||||
<textarea readonly style="width:100%;max-width:100%;height:400px;" id="instances"></textarea>
|
||||
</div>
|
||||
<div style="width:48%;float:right;margin-bottom:20px;">
|
||||
<p>Concepts:</p>
|
||||
<textarea readonly style="width:100%;max-width:100%;height:400px;" id="concepts"></textarea>
|
||||
</div>
|
||||
<p>Sentences:</p>
|
||||
<textarea readonly style="width:100%;max-width:100%;height:400px;" id="cards"></textarea>
|
||||
|
||||
|
||||
<script src="/cenode.js"></script>
|
||||
<script>
|
||||
var text = document.getElementById("input");
|
||||
var send = document.getElementById("send");
|
||||
var send2 = document.getElementById("send2");
|
||||
var concepts = document.getElementById("concepts");
|
||||
var instances = document.getElementById("instances");
|
||||
var cards = document.getElementById("cards");
|
||||
var guess = document.getElementById("guess");
|
||||
|
||||
var node = new CENode(MODELS.CORE);
|
||||
|
||||
function refresh_content(){
|
||||
setTimeout(function(){
|
||||
var c = node.get_concepts();
|
||||
var i = node.get_instances();
|
||||
concepts.value = JSON.stringify(c, undefined, 2);
|
||||
instances.value = JSON.stringify(i, undefined, 2);
|
||||
refresh_content();
|
||||
},200);
|
||||
}
|
||||
refresh_content();
|
||||
|
||||
text.onkeyup = function(event){
|
||||
card.innerHTML = "there is a tell card named 'msg_{uid}' that is from the agent 'test user' and is to the agent 'Moira' and has '"+text.value.replace(/'/g, "\\'")+"' as content and has the timestamp '{now}' as timestamp.";
|
||||
guess.innerHTML = node.guess_next(text.value);
|
||||
|
||||
}
|
||||
text.onkeydown = function(event){
|
||||
if(event.keyCode == 9){
|
||||
text.value = node.guess_next(text.value);
|
||||
event.preventDefault();
|
||||
}
|
||||
if(event.keyCode == 13){
|
||||
send2.click();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
send.onclick = function(){
|
||||
node.add_sentence(card.innerHTML);
|
||||
text.value = "";
|
||||
card.innerHTML = "";
|
||||
};
|
||||
send2.onclick = function(){
|
||||
node.add_sentence(text.value);
|
||||
text.value = "";
|
||||
card.innerHTML = "";
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
25
models/core.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = [
|
||||
"conceptualise an ~ entity ~ E",
|
||||
"conceptualise an ~ imageable thing ~ I that has the value V as ~ image ~",
|
||||
"conceptualise a ~ timestamp ~ T that is an entity",
|
||||
"conceptualise an ~ agent ~ A that is an entity and has the value V as ~ address ~",
|
||||
"conceptualise an ~ individual ~ I that is an ~ agent ~",
|
||||
"conceptualise a ~ card ~ C that is an entity and has the timestamp T as ~ timestamp ~ and has the value V as ~ content ~ and has the value W as ~ linked content ~ and has the value V as ~ number of keystrokes ~ and has the timestamp T as ~ start time ~ and has the value W as ~ submit time ~ and has the value L as ~ latitude ~ and has the value M as ~ longitude ~",
|
||||
"conceptualise the card C ~ is to ~ the agent A and ~ is from ~ the agent B and ~ is in reply to ~ the card C",
|
||||
"conceptualise a ~ tell card ~ T that is a card",
|
||||
"conceptualise an ~ ask card ~ A that is a card",
|
||||
"conceptualise a ~ gist card ~ G that is a card",
|
||||
"conceptualise an ~ nl card ~ N that is a card",
|
||||
"conceptualise a ~ confirm card ~ C that is a card",
|
||||
"conceptualise a ~ location ~ L that is an entity",
|
||||
"conceptualise a ~ locatable thing ~ L that is an entity",
|
||||
"conceptualise the locatable thing L ~ is in ~ the location M",
|
||||
"conceptualise a ~ rule ~ R that is an entity and has the value V as ~ instruction ~",
|
||||
"conceptualise a ~ policy ~ P that is an entity and has the value V as ~ enabled ~ and has the agent A as ~ target ~",
|
||||
"conceptualise a ~ tell policy ~ P that is a policy",
|
||||
"conceptualise an ~ ask policy ~ P that is a policy",
|
||||
"conceptualise a ~ listen policy ~ P that is a policy",
|
||||
"conceptualise a ~ listen onbehalfof policy ~ P that is a policy",
|
||||
"conceptualise a ~ forwardall policy ~ P that is a policy and has the timestamp T as ~ start time ~ and has the value V as ~ all agents ~",
|
||||
"conceptualise a ~ feedback policy ~ P that is a policy and has the value V as ~ acknowledgement ~"
|
||||
];
|
5
models/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
var core = require('./core.js');
|
||||
var server = require('./server.js');
|
||||
var test = require('./test.js');
|
||||
|
||||
module.exports = {core, server, test};
|
3
models/server.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = [
|
||||
"there is a forwardall policy named 'p1' that has 'true' as all agents and has the timestamp '0' as start time and has 'true' as enabled"
|
||||
];
|
7
models/test.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = [
|
||||
"conceptualise an ~ person ~ P that has the value A as ~ age ~",
|
||||
"conceptualise a ~ company ~ C",
|
||||
"conceptualise a ~ barrister ~ B",
|
||||
"conceptualise a ~ londoner ~ L",
|
||||
"conceptualise the person P ~ is married to ~ the person Q and ~ works for ~ the company C",
|
||||
];
|
38
package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "cenode",
|
||||
"version": "3.0.2",
|
||||
"description": "A pure JavaScript implementation of the ITA project's CEStore - called CENode. CENode is able to understand the basic sentence types parsed by the CEStore, such as conceptualising and instance creation and modification.",
|
||||
"homepage": "http://cenode.io",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Will Webberley & Alun Preece",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/flyingsparx/CENode"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"models",
|
||||
"README.md",
|
||||
"LICENCE"
|
||||
],
|
||||
"main": "src/CENode.js",
|
||||
"scripts": {
|
||||
"build-web": "babel src -d lib && webpack --config .webpackrc.js && mv dist/CENode.js dist/cenode.js && mv dist/CEModels.js dist/models.js && uglifyjs dist/cenode.js --mangle -o dist/cenode.min.js --source-map dist/cenode.js.map",
|
||||
"lint-fix": "eslint src/*.js --fix",
|
||||
"lint": "eslint src/*.js",
|
||||
"test": "mocha",
|
||||
"prepublish": "npm test && npm run lint && npm run build-web"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-preset-latest": "^6.16.0",
|
||||
"eslint": "^3.12.2",
|
||||
"eslint-config-airbnb-base": "^11.0.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"expect.js": "^0.3.1",
|
||||
"mocha": "^3.2.0",
|
||||
"uglify-js": "^2.7.5",
|
||||
"webpack": "^1.14.0"
|
||||
}
|
||||
}
|
79
src/CEAgent.js
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const CardHandler = require('./CardHandler.js');
|
||||
const PolicyHandler = require('./PolicyHandler.js');
|
||||
|
||||
const DEFAULT_NAME = 'Moira';
|
||||
|
||||
class CEAgent {
|
||||
|
||||
constructor(node) {
|
||||
if (!node) {
|
||||
throw new Error('CEAgents must be instantiated with a CENode object');
|
||||
}
|
||||
this.node = node;
|
||||
this.handledCards = [];
|
||||
this.cardHandler = new CardHandler(this);
|
||||
this.policyHandler = new PolicyHandler(this);
|
||||
this.setName(DEFAULT_NAME);
|
||||
this.pollCards();
|
||||
this.enactPolicies();
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
this.node.addSentence(`there is an agent named ${name}`);
|
||||
}
|
||||
|
||||
getInstance() {
|
||||
const instances = this.node.getInstances('agent');
|
||||
for (let i = 0; i < instances.length; i += 1) {
|
||||
if (instances[i].name.toLowerCase() === name.toLowerCase()) {
|
||||
return instances[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pollCards() {
|
||||
if (setTimeout) {
|
||||
setTimeout(() => {
|
||||
const cards = this.node.getInstances('card', true);
|
||||
for (const card of cards) {
|
||||
this.cardHandler.handle(card);
|
||||
}
|
||||
this.pollCards();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
enactPolicies() {
|
||||
if (setTimeout) {
|
||||
setTimeout(() => {
|
||||
const policies = this.node.getInstances('policy', true);
|
||||
for (const policy of policies) {
|
||||
this.policyHandler.handle(policy);
|
||||
}
|
||||
this.enactPolicies();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CEAgent;
|
259
src/CEConcept.js
Normal file
@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
class CEConcept {
|
||||
|
||||
constructor(node, name, source) {
|
||||
this.name = name;
|
||||
this.source = source;
|
||||
this.id = node.newConceptId();
|
||||
this.node = node;
|
||||
this.parentIds = [];
|
||||
this.valueIds = [];
|
||||
this.relationshipIds = [];
|
||||
this.synonyms = [];
|
||||
node.concepts.push(this);
|
||||
this.node.conceptDict[this.id] = this;
|
||||
|
||||
const concept = this;
|
||||
Object.defineProperty(node.concepts, name.toLowerCase().replace(/ /g, '_'), {
|
||||
get() {
|
||||
return concept;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
get instances() {
|
||||
const instances = [];
|
||||
for (let i = 0; i < this.node.instances.length; i += 1) {
|
||||
if (this.node.instances[i].type.id === this.id) {
|
||||
instances.push(this.node.instances[i]);
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
get allInstances() {
|
||||
const allConcepts = this.descendants.concat(this);
|
||||
const instances = [];
|
||||
for (let i = 0; i < this.node.instances.length; i += 1) {
|
||||
for (let j = 0; j < allConcepts.length; j += 1) {
|
||||
if (this.node.instances[i].type.id === allConcepts[j].id) {
|
||||
instances.push(this.node.instances[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
get parents() {
|
||||
const p = [];
|
||||
for (let i = 0; i < this.parentIds.length; i += 1) {
|
||||
p.push(this.node.getConceptById(this.parentIds[i]));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
get ancestors() {
|
||||
const parents = [];
|
||||
const stack = [];
|
||||
for (let i = 0; i < this.parentIds.length; i += 1) {
|
||||
stack.push(this.parents[i]);
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
parents.push(current);
|
||||
for (let i = 0; i < current.parents.length; i += 1) {
|
||||
stack.push(current.parents[i]);
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
}
|
||||
|
||||
get children() {
|
||||
const children = [];
|
||||
for (let i = 0; i < this.node.concepts.length; i += 1) {
|
||||
for (let j = 0; j < this.node.concepts[i].parents.length; j += 1) {
|
||||
if (this.node.concepts[i].parents[j].id === this.id) {
|
||||
children.push(this.node.concepts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
get descendants() {
|
||||
const children = [];
|
||||
const stack = [];
|
||||
for (let i = 0; i < this.children.length; i += 1) {
|
||||
stack.push(this.children[i]);
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
children.push(current);
|
||||
const currentChildren = current.children;
|
||||
if (currentChildren) {
|
||||
for (let i = 0; i < currentChildren.length; i += 1) {
|
||||
stack.push(currentChildren[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
get relationships() {
|
||||
const rels = [];
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
const relationship = {};
|
||||
relationship.label = this.relationshipIds[i].label;
|
||||
relationship.concept = this.node.getConceptById(this.relationshipIds[i].target);
|
||||
rels.push(relationship);
|
||||
}
|
||||
return rels;
|
||||
}
|
||||
|
||||
get values() {
|
||||
const vals = [];
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
const value = {};
|
||||
value.label = this.valueIds[i].label;
|
||||
if (this.valueIds[i].conceptId === 0) {
|
||||
value.concept = this.valueIds[i].typeName;
|
||||
} else {
|
||||
value.concept = this.node.getConceptById(this.valueIds[i].type);
|
||||
}
|
||||
vals.push(value);
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
|
||||
get ce() {
|
||||
let ce = `conceptualise a ~ ${this.name} ~ ${this.name.charAt(0).toUpperCase()}`;
|
||||
if (this.parentIds.length > 0 || this.valueIds.length > 0 || this.relationshipIds.length > 0) {
|
||||
ce += ' that';
|
||||
}
|
||||
if (this.parentIds.length > 0) {
|
||||
for (let i = 0; i < this.parentIds.length; i += 1) {
|
||||
ce += ` is a ${this.parentIds[i].name}`;
|
||||
if (i < this.parentIds.length - 1) { ce += ' and'; }
|
||||
}
|
||||
}
|
||||
let facts = [];
|
||||
const alph = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
if (this.valueIds[i].type === 0) {
|
||||
facts.push(`has the value ${alph[i]} as ${this.valueIds[i].label}`);
|
||||
} else {
|
||||
const valType = this.node.getConceptById(this.valueIds[i].type);
|
||||
facts.push(`has the ${valType.name} ${valType.name.charAt(0).toUpperCase()} as ${this.valueIds[i].label}`);
|
||||
}
|
||||
}
|
||||
if (facts.length > 0) {
|
||||
if (this.parentIds.length > 0) { ce += ' and'; }
|
||||
ce += ` ${facts.join(' and ')}`;
|
||||
}
|
||||
ce += '.';
|
||||
if (this.relationshipIds.length > 0) {
|
||||
facts = [];
|
||||
ce += `\nconceptualise the ${this.name} ${this.name.charAt(0).toUpperCase()}`;
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
const relType = this.node.getConceptById(this.relationshipIds[i].target);
|
||||
facts.push(`~ ${this.relationshipIds[i].label} ~ the ${relType.name} ${alph[i]}`);
|
||||
}
|
||||
if (facts.length > 0) {
|
||||
if (this.parentIds.length > 0 || this.valueIds.length > 0) { ce += ' and'; }
|
||||
ce += ` ${facts.join(' and ')}.`;
|
||||
}
|
||||
}
|
||||
return ce;
|
||||
}
|
||||
|
||||
get gist() {
|
||||
let gist = '';
|
||||
if (this.parentIds.length > 0) { gist += `A ${this.name}`; }
|
||||
for (const parentIndex in this.parents) {
|
||||
gist += ` is a type of ${this.parents[parentIndex].name}`;
|
||||
if (parentIndex < this.parents.length - 1) { gist += ' and'; }
|
||||
}
|
||||
if (this.parentIds.length > 0) { gist += '.'; }
|
||||
const facts = [];
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
if (this.valueIds[i].type === 0) {
|
||||
facts.push(`has a value called ${this.valueIds[i].label}`);
|
||||
} else {
|
||||
const valType = this.node.getConceptById(this.valueIds[i].type);
|
||||
facts.push(`has a type of ${valType.name} called ${this.valueIds[i].label}`);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
const relType = this.node.getConceptById(this.relationshipIds[i].target);
|
||||
facts.push(`${this.relationshipIds[i].label} a type of ${relType.name}`);
|
||||
}
|
||||
if (facts.length > 0) {
|
||||
gist += ` An instance of ${this.name} ${facts.join(' and ')}.`;
|
||||
} else if (facts.length === 0 && this.parents.length === 0) {
|
||||
gist += `A ${this.name} has no attributes or relationships.`;
|
||||
}
|
||||
return gist;
|
||||
}
|
||||
|
||||
addValue(label, type, source) {
|
||||
const value = {};
|
||||
value.source = source;
|
||||
value.label = label;
|
||||
value.type = typeof type === 'number' ? type : type.id;
|
||||
this.valueIds.push(value);
|
||||
Object.defineProperty(this, label.toLowerCase().replace(/ /g, '_'), {
|
||||
get() {
|
||||
return type === 0 ? 'value' : type;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
addRelationship(label, target, source) {
|
||||
const relationship = {};
|
||||
relationship.source = source;
|
||||
relationship.label = label;
|
||||
relationship.target = target.id;
|
||||
this.relationshipIds.push(relationship);
|
||||
Object.defineProperty(this, label.toLowerCase().replace(/ /g, '_'), {
|
||||
get() {
|
||||
return target;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
addParent(parentConcept) {
|
||||
if (this.parentIds.indexOf(parentConcept.id) === -1) {
|
||||
this.parentIds.push(parentConcept.id);
|
||||
}
|
||||
}
|
||||
|
||||
addSynonym(synonym) {
|
||||
for (let i = 0; i < this.synonyms.length; i += 1) {
|
||||
if (this.synonyms[i].toLowerCase() === synonym.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.synonyms.push(synonym);
|
||||
}
|
||||
}
|
||||
module.exports = CEConcept;
|
335
src/CEInstance.js
Normal file
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
class CEInstance {
|
||||
|
||||
constructor(node, type, name, source) {
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.node = node;
|
||||
this.name = name;
|
||||
this.source = source;
|
||||
this.id = node.newInstanceId();
|
||||
this.concept = type;
|
||||
this.conceptId = type.id;
|
||||
this.subConcepts = [];
|
||||
this.sentences = [];
|
||||
this.valueIds = [];
|
||||
this.relationshipIds = [];
|
||||
this.synonyms = [];
|
||||
this.reservedFields = ['values', 'relationships', 'synonyms', 'addValue', 'addRelationship', 'name', 'concept', 'id', 'instance', 'sentences', 'ce', 'gist'];
|
||||
node.instances.push(this);
|
||||
this.node.instanceDict[this.id] = this;
|
||||
|
||||
const instance = this;
|
||||
Object.defineProperty(node.instances, name.toLowerCase().replace(/ /g, '_').replace(/'/g, ''), {
|
||||
get() {
|
||||
return instance;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(type, name.toLowerCase(), {
|
||||
get() {
|
||||
return instance;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
get type() {
|
||||
for (let i = 0; i < this.node.concepts.length; i += 1) {
|
||||
if (this.node.concepts[i].id === this.concept.id) {
|
||||
return this.node.concepts[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get relationships() {
|
||||
const rels = [];
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
const relationship = {};
|
||||
relationship.label = this.relationshipIds[i].label;
|
||||
relationship.source = this.relationshipIds[i].source;
|
||||
relationship.instance = this.node.getInstanceById(this.relationshipIds[i].targetId);
|
||||
rels.push(relationship);
|
||||
}
|
||||
return rels;
|
||||
}
|
||||
|
||||
get values() {
|
||||
const vals = [];
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
const value = {};
|
||||
value.label = this.valueIds[i].label;
|
||||
value.source = this.valueIds[i].source;
|
||||
if (this.valueIds[i].conceptId === 0) {
|
||||
value.instance = this.valueIds[i].typeName;
|
||||
} else {
|
||||
value.instance = this.node.getInstanceById(this.valueIds[i].conceptId);
|
||||
}
|
||||
vals.push(value);
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
|
||||
addSentence(sentence) {
|
||||
this.sentences.push(sentence);
|
||||
}
|
||||
|
||||
getPossibleProperties() {
|
||||
const ancestorInstances = this.concept.ancestors;
|
||||
ancestorInstances.push(this.concept);
|
||||
const properties = { values: [], relationships: [] };
|
||||
for (let i = 0; i < ancestorInstances.length; i += 1) {
|
||||
for (let j = 0; j < ancestorInstances[i].values.length; j += 1) {
|
||||
properties.values.push(ancestorInstances[i].values[j].label.toLowerCase());
|
||||
}
|
||||
for (let j = 0; j < ancestorInstances[i].relationships.length; j += 1) {
|
||||
properties.relationships.push(ancestorInstances[i].relationships[j].label.toLowerCase());
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
addValue(label, valueInstance, propagate, source) {
|
||||
if (!(label && label.length && valueInstance)) {
|
||||
return null;
|
||||
}
|
||||
if (this.getPossibleProperties().values.indexOf(label.toLowerCase()) > -1) {
|
||||
const value = {};
|
||||
value.source = source;
|
||||
value.label = label;
|
||||
value.conceptId = typeof valueInstance === 'object' ? valueInstance.id : 0;
|
||||
value.typeName = typeof valueInstance === 'object' ? valueInstance.name : valueInstance;
|
||||
this.valueIds.push(value);
|
||||
const valueNameField = label.toLowerCase().replace(/ /g, '_');
|
||||
|
||||
if (this.reservedFields.indexOf(valueNameField) === -1) {
|
||||
Object.defineProperty(this, valueNameField, {
|
||||
get() {
|
||||
return value.conceptId === 0 ? value.typeName : this.node.getInstanceById(value.conceptId);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
if (this.reservedFields.indexOf(`${valueNameField}s`) === -1 && !Object.prototype.hasOwnProperty.call(this, `${valueNameField}s`)) {
|
||||
Object.defineProperty(this, `${valueNameField}s`, {
|
||||
get() {
|
||||
const instances = [];
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
if (this.valueIds[i].label.toLowerCase().replace(/ /g, '_') === valueNameField) {
|
||||
instances.push(this.valueIds[i].conceptId === 0 ? this.valueIds[i].typeName : this.node.getInstanceById(this.valueIds[i].conceptId));
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (propagate !== false) {
|
||||
this.node.ruleEngine.enactRules(this, 'value', valueInstance, source);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
addRelationship(label, relationshipInstance, propagate, source) {
|
||||
if (this.getPossibleProperties().relationships.indexOf(label.toLowerCase()) > -1) {
|
||||
const relationship = {};
|
||||
relationship.label = label;
|
||||
relationship.source = source;
|
||||
relationship.targetId = relationshipInstance.id;
|
||||
relationship.targetName = relationshipInstance.name;
|
||||
this.relationshipIds.push(relationship);
|
||||
const relNameField = label.toLowerCase().replace(/ /g, '_');
|
||||
|
||||
if (this.reservedFields.indexOf(relNameField) === -1) {
|
||||
Object.defineProperty(this, relNameField, {
|
||||
get() {
|
||||
return this.node.getInstanceById(relationship.targetId);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
if (this.reservedFields.indexOf(`${relNameField}s`) === -1 && !Object.prototype.hasOwnProperty.call(this, `${relNameField}s`)) {
|
||||
Object.defineProperty(this, `${relNameField}s`, {
|
||||
get() {
|
||||
const instances = [];
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
if (this.relationshipIds[i].label.toLowerCase().replace(/ /g, '_') === relNameField) {
|
||||
instances.push(this.node.getInstanceById(this.relationshipIds[i].targetId));
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (propagate !== false) {
|
||||
this.node.ruleEngine.enactRules(this, 'relationship', relationshipInstance, source);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
addSynonym(synonym) {
|
||||
if (!synonym || !synonym.length) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.synonyms.length; i += 1) {
|
||||
if (this.synonyms[i].toLowerCase() === synonym.toLowerCase()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this.synonyms.push(synonym);
|
||||
Object.defineProperty(this, synonym.toLowerCase().replace(/ /g, '_'), {
|
||||
get() {
|
||||
return this;
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
addSubConcept(concept) {
|
||||
if (!concept) {
|
||||
return;
|
||||
}
|
||||
let add = true;
|
||||
for (const existingConcept of this.subConcepts) {
|
||||
if (existingConcept.id === concept.id || concept.id === this.concept.id) {
|
||||
add = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (add) {
|
||||
this.subConcepts.push(concept);
|
||||
}
|
||||
}
|
||||
|
||||
property(propertyName, source) {
|
||||
return this.properties(propertyName, source, true);
|
||||
}
|
||||
|
||||
properties(propertyName, source, onlyOne) {
|
||||
const properties = [];
|
||||
for (let i = this.values.length - 1; i >= 0; i -= 1) { // Reverse so we get the latest prop first
|
||||
if (this.values[i].label.toLowerCase() === propertyName.toLowerCase()) {
|
||||
const inst = this.values[i].instance;
|
||||
const dat = source ? { instance: inst, source: this.values[i].source } : inst;
|
||||
if (onlyOne) { return dat; }
|
||||
properties.push(dat);
|
||||
}
|
||||
}
|
||||
for (let i = this.relationships.length - 1; i >= 0; i -= 1) { // Reverse so we get the latest prop first
|
||||
if (this.relationships[i].label.toLowerCase() === propertyName.toLowerCase()) {
|
||||
const inst = this.relationships[i].instance;
|
||||
const dat = source ? { instance: inst, source: this.relationships[i].source } : inst;
|
||||
if (onlyOne) { return dat; }
|
||||
properties.push(dat);
|
||||
}
|
||||
}
|
||||
return onlyOne ? null : properties;
|
||||
}
|
||||
|
||||
get ce() {
|
||||
const concept = this.concept;
|
||||
if (!concept) { return ''; }
|
||||
let ce = `there is a ${concept.name} named '${this.name}'`;
|
||||
const facts = [];
|
||||
for (const subConcept of this.subConcepts) {
|
||||
facts.push(`is a ${subConcept.name}`);
|
||||
}
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
const value = this.valueIds[i];
|
||||
if (value.conceptId === 0) {
|
||||
facts.push(`has '${value.typeName.replace(/'/g, "\\'")}' as ${value.label}`);
|
||||
} else {
|
||||
const valueInstance = this.node.getInstanceById(value.conceptId);
|
||||
const valueConcept = valueInstance.type;
|
||||
facts.push(`has the ${valueConcept.name} '${valueInstance.name}' as ${value.label}`);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
const relationship = this.relationshipIds[i];
|
||||
const relationshipInstance = this.node.getInstanceById(relationship.targetId);
|
||||
const relationshipConcept = relationshipInstance.type;
|
||||
facts.push(`${relationship.label} the ${relationshipConcept.name} '${relationshipInstance.name}'`);
|
||||
}
|
||||
if (facts.length > 0) { ce += ` that ${facts.join(' and ')}`; }
|
||||
return `${ce}.`;
|
||||
}
|
||||
|
||||
get gist() {
|
||||
const vowels = ['a', 'e', 'i', 'o', 'u'];
|
||||
const concept = this.concept;
|
||||
if (!concept) { return ''; }
|
||||
let gist = `${this.name} is`;
|
||||
if (vowels.indexOf(concept.name.toLowerCase()[0]) > -1) { gist += ` an ${concept.name}.`; } else { gist += ` a ${concept.name}`; }
|
||||
for (let i = 0; i < this.subConcepts.length; i += 1) {
|
||||
gist += ` and a ${this.subConcepts[i].name}`;
|
||||
}
|
||||
gist += '.';
|
||||
|
||||
const facts = {};
|
||||
let factFound = false;
|
||||
for (let i = 0; i < this.valueIds.length; i += 1) {
|
||||
factFound = true;
|
||||
const value = this.valueIds[i];
|
||||
let fact = '';
|
||||
if (value.conceptId === 0) {
|
||||
fact = `has '${value.typeName.replace(/'/g, "\\'")}' as ${value.label}`;
|
||||
} else {
|
||||
const valueInstance = this.node.getInstanceById(value.conceptId);
|
||||
const valueConcept = valueInstance.type;
|
||||
fact = `has the ${valueConcept.name} '${valueInstance.name}' as ${value.label}`;
|
||||
}
|
||||
if (!(fact in facts)) {
|
||||
facts[fact] = 0;
|
||||
}
|
||||
facts[fact] += 1;
|
||||
}
|
||||
for (let i = 0; i < this.relationshipIds.length; i += 1) {
|
||||
factFound = true;
|
||||
const relationship = this.relationshipIds[i];
|
||||
const relationshipInstance = this.node.getInstanceById(relationship.targetId);
|
||||
const relationshipConcept = relationshipInstance.type;
|
||||
const fact = `${relationship.label} the ${relationshipConcept.name} '${relationshipInstance.name}'`;
|
||||
if (!(fact in facts)) {
|
||||
facts[fact] = 0;
|
||||
}
|
||||
facts[fact] += 1;
|
||||
}
|
||||
if (factFound) {
|
||||
gist += ` ${this.name}`;
|
||||
for (const fact in facts) {
|
||||
gist += ` ${fact}`;
|
||||
if (facts[fact] > 1) {
|
||||
gist += ` (${facts[fact]} times)`;
|
||||
}
|
||||
gist += ' and';
|
||||
}
|
||||
gist = `${gist.substring(0, gist.length - 4)}.`; // Remove last ' and' and add full stop
|
||||
}
|
||||
return gist;
|
||||
}
|
||||
}
|
||||
module.exports = CEInstance;
|
258
src/CENode.js
Executable file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const CEAgent = require('./CEAgent.js');
|
||||
const CEParser = require('./CEParser.js');
|
||||
const QuestionParser = require('./QuestionParser.js');
|
||||
const NLParser = require('./NLParser.js');
|
||||
const RuleEngine = require('./RuleEngine.js');
|
||||
|
||||
class CENode {
|
||||
|
||||
newInstanceId() {
|
||||
this.lastInstanceId += 1;
|
||||
return this.lastInstanceId;
|
||||
}
|
||||
newConceptId() {
|
||||
this.lastConceptId += 1;
|
||||
return this.lastConceptId;
|
||||
}
|
||||
newCardId() {
|
||||
if (!this.agent) {
|
||||
return null;
|
||||
}
|
||||
this.lastCardId += 1;
|
||||
return this.agent.name + this.lastCardId;
|
||||
}
|
||||
|
||||
getConceptById(id) {
|
||||
return this.conceptDict[id];
|
||||
}
|
||||
|
||||
getConceptByName(name) {
|
||||
if (!name) { return null; }
|
||||
for (let i = 0; i < this.concepts.length; i += 1) {
|
||||
if (this.concepts[i].name.toLowerCase() === name.toLowerCase()) {
|
||||
return this.concepts[i];
|
||||
}
|
||||
for (let j = 0; j < this.concepts[i].synonyms.length; j += 1) {
|
||||
if (this.concepts[i].synonyms[j].toLowerCase() === name.toLowerCase()) {
|
||||
return this.concepts[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getInstanceById(id) {
|
||||
return this.instanceDict[id];
|
||||
}
|
||||
|
||||
getInstanceByName(name) {
|
||||
if (!name) { return null; }
|
||||
for (let i = 0; i < this.instances.length; i += 1) {
|
||||
if (this.instances[i].name.toLowerCase() === name.toLowerCase()) {
|
||||
return this.instances[i];
|
||||
}
|
||||
for (let j = 0; j < this.instances[i].synonyms.length; j += 1) {
|
||||
if (this.instances[i].synonyms[j].toLowerCase() === name.toLowerCase()) {
|
||||
return this.instances[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Get the current set of instances maintained by the node.
|
||||
*
|
||||
* If conceptType and recurse NULL:
|
||||
* - Return ALL instances
|
||||
*
|
||||
* If conceptType not NULL and recurse NULL|FALSE:
|
||||
* - Return all instances with concept type name 'conceptType'
|
||||
*
|
||||
* If recurse TRUE:
|
||||
* - Return all instances of concepts that are children, grandchildren, etc.
|
||||
* of concept with name 'conceptType'
|
||||
*
|
||||
* Returns: [obj{instance}]
|
||||
*/
|
||||
getInstances(conceptType, recurse) {
|
||||
let instanceList = [];
|
||||
if (!conceptType) {
|
||||
instanceList = this.instances;
|
||||
} else if (conceptType && !recurse) {
|
||||
const concept = this.getConceptByName(conceptType);
|
||||
if (concept) {
|
||||
for (let i = 0; i < this.instances.length; i += 1) {
|
||||
if (this.instances[i].type.id === concept.id) {
|
||||
instanceList.push(this.instances[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (conceptType && recurse === true) {
|
||||
const concept = this.getConceptByName(conceptType);
|
||||
if (concept) {
|
||||
const descendants = concept.descendants.concat(concept);
|
||||
const childrenIds = [];
|
||||
for (let i = 0; i < descendants.length; i += 1) { childrenIds.push(descendants[i].id); }
|
||||
for (let i = 0; i < this.instances.length; i += 1) {
|
||||
if (childrenIds.indexOf(this.instances[i].type.id) > -1) {
|
||||
instanceList.push(this.instances[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return instanceList;
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds a sentence to be processed by the node.
|
||||
* This method will ALWAYS return a response by dynamically
|
||||
* checking whether input is pure CE -> question -> NL.
|
||||
*
|
||||
* Returns: see signatures for addCE, askQuestion, addNL
|
||||
*/
|
||||
addSentence(sentence, source) {
|
||||
const ceResult = this.addCE(sentence, false, source);
|
||||
if (ceResult.success) {
|
||||
return ceResult;
|
||||
}
|
||||
|
||||
const questionResult = this.askQuestion(sentence);
|
||||
if (questionResult.success) {
|
||||
return questionResult;
|
||||
}
|
||||
|
||||
return this.addNL(sentence);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an array of sentences to the node.
|
||||
*
|
||||
* Returns: [[bool, str]...] (see signature for addSentence)
|
||||
*/
|
||||
addSentences(sentences, source) {
|
||||
const responses = [];
|
||||
for (let i = 0; i < sentences.length; i += 1) {
|
||||
responses.push(this.addSentence(sentences[i], source));
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
/*
|
||||
* Attempt to parse CE and add data to the node.
|
||||
* Indicates whether CE was successfully parsed.
|
||||
*
|
||||
*
|
||||
* Returns: {success: bool, type: str, data: str}
|
||||
*/
|
||||
addCE(sentence, source) {
|
||||
const success = this.ceParser.parse(sentence.trim().replace('{now}', new Date().getTime()).replace('{uid}', this.newCardId()), source);
|
||||
return {
|
||||
success: success[0],
|
||||
type: 'gist',
|
||||
data: success[1],
|
||||
result: success[2] || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Attempt to query the node. Success is indicated.
|
||||
* Indicates success of whether a valid question was parsed
|
||||
*
|
||||
* Returns: {success: bool, type: str, data: str}
|
||||
*/
|
||||
askQuestion(sentence) {
|
||||
const success = this.questionParser.parse(sentence);
|
||||
return {
|
||||
success: success[0],
|
||||
type: success[0] ? 'gist' : undefined,
|
||||
data: success[0] ? success[1] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Attempt to parse NL without updating model.
|
||||
* Method returns a response representing a CE 'guess' of the input sentence
|
||||
*
|
||||
* Returns: {type: str, data: str}
|
||||
*/
|
||||
addNL(sentence) {
|
||||
const success = this.nlParser.parse(sentence);
|
||||
return {
|
||||
type: success[0] ? 'confirm' : 'gist',
|
||||
data: success[1],
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an array of CE sentences to the node.
|
||||
*
|
||||
* Returns: [[bool, str]...] (see signature for addCE)
|
||||
*/
|
||||
loadModel(sentences) {
|
||||
const responses = [];
|
||||
for (let i = 0; i < sentences.length; i += 1) {
|
||||
responses.push(this.addCE(sentences[i]));
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
/*
|
||||
* Reset store to 'factory settings' by removing all known instances
|
||||
* and concepts.
|
||||
*/
|
||||
resetAll() {
|
||||
this.instances = [];
|
||||
this.concepts = [];
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialise and attach a new CEAgent to handle
|
||||
* cards and policies for the node.
|
||||
*/
|
||||
attachAgent(agent) {
|
||||
this.agent = agent || new CEAgent(this);
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialise node by adding any passed models as
|
||||
* sentence sets to be processed.
|
||||
*/
|
||||
constructor(...models) {
|
||||
this.ceParser = new CEParser(this);
|
||||
this.questionParser = new QuestionParser(this);
|
||||
this.nlParser = new NLParser(this);
|
||||
this.ruleEngine = new RuleEngine(this);
|
||||
this.concepts = [];
|
||||
this.instances = [];
|
||||
this.conceptDict = {};
|
||||
this.instanceDict = {};
|
||||
this.conceptIds = {};
|
||||
this.lastInstanceId = this.instances.length;
|
||||
this.lastConceptId = this.concepts.length;
|
||||
this.lastCardId = 0;
|
||||
for (let i = 0; i < models.length; i += 1) {
|
||||
this.loadModel(models[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = CENode;
|
244
src/CEParser.js
Normal file
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const CEConcept = require('./CEConcept.js');
|
||||
const CEInstance = require('./CEInstance.js');
|
||||
|
||||
const quotes = {
|
||||
escape(string) {
|
||||
return string.replace(/'/g, "\\'");
|
||||
},
|
||||
unescape(string) {
|
||||
return string.replace(/\\'/g, "'").replace(/^'/, '').replace(/'$/, '');
|
||||
},
|
||||
};
|
||||
|
||||
class CEParser {
|
||||
|
||||
/*
|
||||
* Submit CE to be processed by node.
|
||||
* This may result in
|
||||
* - new concepts or instances being created
|
||||
* - modifications to existing concepts or instances
|
||||
* - no action (i.e. invalid)
|
||||
*
|
||||
* Source is an optional identifier to tag stored information with an input source.
|
||||
*
|
||||
* Returns: [bool, str] (bool = success, str = error or parsed string)
|
||||
*/
|
||||
parse(input, source) {
|
||||
const t = input.replace(/\s+/g, ' ').replace(/\.+$/, '').trim(); // Whitespace -> single space
|
||||
if (t.match(/^conceptualise an?/i)) {
|
||||
return this.newConcept(t, source);
|
||||
} else if (t.match(/^conceptualise the/i)) {
|
||||
return this.modifyConcept(t, source);
|
||||
} else if (t.match(/^there is an? ([a-zA-Z0-9 ]*) named/i)) {
|
||||
return this.newInstance(t, source);
|
||||
} else if (t.match(/^the ([a-zA-Z0-9 ]*)/i)) {
|
||||
return this.modifyInstance(t, source);
|
||||
}
|
||||
return [false, null];
|
||||
}
|
||||
|
||||
newConcept(t, source) {
|
||||
const match = t.match(/^conceptualise an? ~ ([a-zA-Z0-9 ]*) ~ ([A-Z0-9]+)/i);
|
||||
const conceptName = match[1];
|
||||
const storedConcept = this.node.getConceptByName(conceptName);
|
||||
let concept = null;
|
||||
if (storedConcept) {
|
||||
return [false, 'This concept already exists.'];
|
||||
}
|
||||
concept = new CEConcept(this.node, conceptName, source);
|
||||
|
||||
const remainder = t.replace(/^conceptualise an? ~ ([a-zA-Z0-9 ]*) ~ ([A-Z0-9]+) that/, '');
|
||||
const facts = remainder.replace(/\band\b/g, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
|
||||
for (const fact of facts) {
|
||||
this.processConceptFact(concept, fact, source);
|
||||
}
|
||||
return [true, t, concept];
|
||||
}
|
||||
|
||||
modifyConcept(t, source) {
|
||||
const conceptInfo = t.match(/^conceptualise the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) (?:has|is|~)/);
|
||||
const conceptName = conceptInfo[1];
|
||||
const conceptVar = conceptInfo[2];
|
||||
const concept = this.node.getConceptByName(conceptName);
|
||||
if (!concept) {
|
||||
return [false, `Concept ${conceptInfo[1]} not known.`];
|
||||
}
|
||||
|
||||
const remainderRegex = new RegExp(`^conceptualise the ${conceptName} ${conceptVar}`, 'i');
|
||||
const remainder = t.replace(remainderRegex, '');
|
||||
const facts = remainder.replace(/\band\b/g, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
|
||||
for (const fact of facts) {
|
||||
this.processConceptFact(concept, fact, source);
|
||||
}
|
||||
return [true, t, concept];
|
||||
}
|
||||
|
||||
processConceptFact(concept, fact, source) {
|
||||
const input = fact.trim().replace(/\+/g, 'and');
|
||||
if (input.match(/has the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) as ~ ([a-zA-Z0-9 ]*) ~/g)) {
|
||||
const re = /has the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) as ~ ([a-zA-Z0-9 ]*) ~/g;
|
||||
const match = re.exec(input);
|
||||
const valConceptName = match[1];
|
||||
const label = match[3];
|
||||
const valConcept = valConceptName === 'value' ? 0 : this.node.getConceptByName(valConceptName);
|
||||
concept.addValue(label, valConcept, source);
|
||||
}
|
||||
if (input.match(/^is an? ([a-zA-Z0-9 ]*)/)) {
|
||||
const re = /^is an? ([a-zA-Z0-9 ]*)/;
|
||||
const match = re.exec(input);
|
||||
const parentConceptName = match[1];
|
||||
const parentConcept = this.node.getConceptByName(parentConceptName);
|
||||
if (parentConcept) {
|
||||
concept.addParent(parentConcept);
|
||||
}
|
||||
}
|
||||
if (input.match(/~ ([a-zA-Z0-9 ]*) ~ the ([a-zA-Z0-9 ]*) ([A-Z0-9]+)/)) {
|
||||
const re = /~ ([a-zA-Z0-9 ]*) ~ the ([a-zA-Z0-9 ]*) ([A-Z0-9]+)/;
|
||||
const match = re.exec(input);
|
||||
const label = match[1];
|
||||
const relConceptName = match[2];
|
||||
const relConcept = this.node.getConceptByName(relConceptName);
|
||||
if (relConcept) {
|
||||
concept.addRelationship(label, relConcept, source);
|
||||
}
|
||||
}
|
||||
if (input.match(/~ is expressed by ~ ([a-zA-Z0-9 ]*)/)) {
|
||||
const re = /~ is expressed by ~ ([a-zA-Z0-9 ]*)/;
|
||||
const match = re.exec(input);
|
||||
const synonym = match[1];
|
||||
concept.addSynonym(synonym);
|
||||
}
|
||||
}
|
||||
|
||||
newInstance(t, source) {
|
||||
let names = t.match(/^there is an? ([a-zA-Z0-9 ]*) named '([^'\\]*(?:\\.[^'\\]*)*)'/i);
|
||||
if (!names) {
|
||||
names = t.match(/^there is an? ([a-zA-Z0-9 ]*) named ([a-zA-Z0-9_]*)/i);
|
||||
if (!names) { return [false, 'Unable to determine name of instance.']; }
|
||||
}
|
||||
const conceptName = names[1];
|
||||
const instanceName = names[2].replace(/\\/g, '');
|
||||
const concept = this.node.getConceptByName(conceptName);
|
||||
const currentInstance = this.node.getInstanceByName(instanceName);
|
||||
const instance = new CEInstance(this.node, concept, instanceName, source);
|
||||
if (!concept) {
|
||||
return [false, `Instance type unknown: ${conceptName}`];
|
||||
}
|
||||
if (currentInstance && currentInstance.type.id === concept.id) {
|
||||
return [true, 'There is already an instance of this type with this name.', currentInstance];
|
||||
}
|
||||
instance.sentences.push(t);
|
||||
|
||||
const remainder = t.replace(/^there is an? (?:[a-zA-Z0-9 ]*) named (?:[a-zA-Z0-9_]*|'[a-zA-Z0-9_ ]*') that/, '');
|
||||
const facts = remainder.replace(/\band\b/g, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
|
||||
for (const fact of facts) {
|
||||
this.processInstanceFact(instance, fact, source);
|
||||
}
|
||||
return [true, t, instance];
|
||||
}
|
||||
|
||||
modifyInstance(t, source) {
|
||||
let concept;
|
||||
let instance;
|
||||
if (t.match(/^the ([a-zA-Z0-9 ]*) '([^'\\]*(?:\\.[^'\\]*)*)'/i)) {
|
||||
const names = t.match(/^the ([a-zA-Z0-9 ]*) '([^'\\]*(?:\\.[^'\\]*)*)'/i);
|
||||
if (names) {
|
||||
concept = this.node.getConceptByName(names[1]);
|
||||
instance = this.node.getInstanceByName(names[2].replace(/\\/g, ''));
|
||||
}
|
||||
}
|
||||
if (!instance && t.match(/^the ([a-zA-Z0-9 ]*)/i)) {
|
||||
const names = t.match(/^the ([a-zA-Z0-9 ]*)/i);
|
||||
const nameTokens = names[1].split(' ');
|
||||
for (let i = 0; i < this.node.concepts.length; i += 1) {
|
||||
if (names[1].toLowerCase().indexOf(this.node.concepts[i].name.toLowerCase()) === 0) {
|
||||
concept = this.node.concepts[i];
|
||||
const instanceName = nameTokens[concept.name.split(' ').length].toLowerCase();
|
||||
instance = concept[instanceName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!concept || !instance) {
|
||||
return [false, `Unknown concept/instance combination in: ${t}`];
|
||||
}
|
||||
instance.sentences.push(t);
|
||||
|
||||
const remainder = t.replace(`'${instance.name}'`, instance.name).replace(`the ${concept.name} ${instance.name}`.trim(), '');
|
||||
const facts = remainder.replace(/\band\b/g, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
|
||||
for (const fact of facts) {
|
||||
this.processInstanceFact(instance, fact, source);
|
||||
}
|
||||
return [true, t, instance];
|
||||
}
|
||||
|
||||
processInstanceFact(instance, fact, source) {
|
||||
const input = fact.trim().replace(/\+/g, 'and');
|
||||
if (input.match(/^(?!has)([a-zA-Z0-9 ]*) the ([a-zA-Z0-9 ]*) ([a-zA-Z0-9_' ]*)/)) {
|
||||
const re = /^(?!has)([a-zA-Z0-9 ]*) the ([a-zA-Z0-9 ]*) ([a-zA-Z0-9_' ]*)/;
|
||||
const match = re.exec(input);
|
||||
const label = match[1];
|
||||
const relConceptName = match[2];
|
||||
const relInstanceName = match[3].replace(/'/g, '');
|
||||
const relConcept = this.node.getConceptByName(relConceptName);
|
||||
let relInstance = this.node.getInstanceByName(relInstanceName);
|
||||
if (!relInstance) {
|
||||
relInstance = new CEInstance(this.node, relConcept, relInstanceName, source);
|
||||
}
|
||||
instance.addRelationship(label, relInstance, true, source);
|
||||
}
|
||||
if (input.match(/^has ([a-zA-Z0-9]*|'[^'\\]*(?:\\.[^'\\]*)*') as ([a-zA-Z0-9 ]*)/)) {
|
||||
const re = /^has ([a-zA-Z0-9]*|'[^'\\]*(?:\\.[^'\\]*)*') as ([a-zA-Z0-9 ]*)/;
|
||||
const match = re.exec(input);
|
||||
const value = quotes.unescape(match[1]);
|
||||
const label = match[2];
|
||||
instance.addValue(label, value, true, source);
|
||||
}
|
||||
if (input.match(/^has the ([a-zA-Z0-9 ]*) ([a-zA-Z0-9_]*|'[a-zA-Z0-9_ ]*') as ([a-zA-Z0-9 ]*)/)) {
|
||||
const re = /^has the ([a-zA-Z0-9 ]*) ([a-zA-Z0-9]*|'[a-zA-Z0-9 ]*') as ([a-zA-Z0-9 ]*)/;
|
||||
const match = re.exec(input);
|
||||
const valConceptName = match[1];
|
||||
const valInstanceName = match[2].replace(/'/g, '');
|
||||
const label = match[3];
|
||||
const valConcept = this.node.getConceptByName(valConceptName);
|
||||
let valInstance = this.node.getInstanceByName(valInstanceName);
|
||||
if (!valInstance) {
|
||||
valInstance = new CEInstance(this.node, valConcept, valInstanceName, source);
|
||||
}
|
||||
instance.addValue(label, valInstance, true, source);
|
||||
}
|
||||
if (input.match(/(?:is| )?an? ([a-zA-Z0-9 ]*)/g)) {
|
||||
const re = /(?:is| )?an? ([a-zA-Z0-9 ]*)/g;
|
||||
const match = re.exec(input);
|
||||
instance.addSubConcept(this.node.getConceptByName(match && match[1] && match[1].trim()));
|
||||
}
|
||||
if (input.match(/is expressed by ('[a-zA-Z0-9 ]*'|[a-zA-Z0-9]*)/)) {
|
||||
const match = input.match(/is expressed by ('[a-zA-Z0-9 ]*'|[a-zA-Z0-9]*)/);
|
||||
const synonym = match && match[1] && match[1].replace(/'/g, '').trim();
|
||||
instance.addSynonym(synonym);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
module.exports = CEParser;
|
160
src/CEServer.js
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const CENode = require('./CENode.js');
|
||||
const CEModels = require('../models/index.js');
|
||||
|
||||
const POST_SENTENCES_ENDPOINT = '/sentences';
|
||||
const GET_CARDS_ENDPOINT = '/cards';
|
||||
|
||||
function postSentences(node, request, response) {
|
||||
let body = '';
|
||||
request.on('data', (chunk) => { body += chunk; });
|
||||
request.on('end', () => {
|
||||
body = decodeURIComponent(body.replace('sentence=', '').replace(/\+/g, ' '));
|
||||
const sentences = body.split(/\\n|\n/);
|
||||
const responses = node.addSentences(sentences);
|
||||
response.write(responses.map(resp => resp.data).join('\n'));
|
||||
response.end();
|
||||
});
|
||||
}
|
||||
|
||||
function getCards(node, request, response, ignoresInput) {
|
||||
const url = decodeURIComponent(request.url);
|
||||
const agentRegex = url.match(/agent=(.*)/);
|
||||
const ignores = ignoresInput || [];
|
||||
let agentStr = null;
|
||||
let agents = [];
|
||||
if (agentRegex) { agentStr = agentRegex[1]; }
|
||||
if (agentStr) {
|
||||
agents = agentStr.toLowerCase().split(',');
|
||||
}
|
||||
const cards = node.getInstances('card', true);
|
||||
let s = '';
|
||||
for (let i = 0; i < cards.length; i += 1) {
|
||||
if (ignores.indexOf(cards[i].name) === -1) {
|
||||
if (!agents || agents.length === 0) {
|
||||
s += `${cards[i].ce}\n`;
|
||||
} else {
|
||||
const tos = cards[i].is_tos;
|
||||
if (tos) {
|
||||
for (let j = 0; j < tos.length; j += 1) {
|
||||
for (let k = 0; k < agents.length; k += 1) {
|
||||
if (tos[j].name.toLowerCase() === agents[k]) {
|
||||
s += `${cards[i].ce}\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response.write(s);
|
||||
response.end();
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
const node = new CENode(CEModels.core, CEModels.server);
|
||||
node.attachAgent();
|
||||
let port = 5555;
|
||||
if (process.argv.length > 3) {
|
||||
port = process.argv[3];
|
||||
}
|
||||
if (process.argv.length > 2) {
|
||||
node.agent.setName(process.argv[2]);
|
||||
}
|
||||
|
||||
http.createServer((request, response) => {
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (request.method === 'GET') {
|
||||
if (request.url === '/') {
|
||||
const ins = node.instances;
|
||||
const con = node.concepts;
|
||||
let s = '<html><head><title>CENode Management</title></head><body><h1>CENode Server Admin Interface</h1>';
|
||||
s += '<div style="width:48%;float:left;"><h2>Conceptual model</h2>';
|
||||
s += '<p>Add CE sentences to the node:</p><form action="/ui/sentences" enctype="application/x-www-form-urlencoded" method="POST"><textarea name="sentence" style="width:95%;height:100px;"></textarea><br /><br /><input type="submit" /></form></div>';
|
||||
s += `<div style="width:48%;float:left;"><h2>Node settings</h2><p>Update local agent name:</p><form method="POST" action="/agent-name"><input type="text" name="name" value="${node.agent.name}" /><input type="submit" /></form>`;
|
||||
s += '<p>Other options:</p><button onclick="window.location=\'/reset\';">Empty model</button>';
|
||||
s += '</div><div style="clear:both;"></div>';
|
||||
s += '<div style="display:inline-block;width:45%;float:left;"><h2>Concepts</h2>';
|
||||
for (const concept of con) {
|
||||
s += concept.name;
|
||||
if (concept.parents.length) {
|
||||
s += ` (${concept.parents[0].name})`;
|
||||
}
|
||||
s += '<br>';
|
||||
}
|
||||
s += '</div><div style="display:inline-block;width:45%;float:right;"><h2>Instances</h2>';
|
||||
for (const instance of ins) {
|
||||
s += `${instance.name} (${instance.type.name})<br>`;
|
||||
}
|
||||
s += '</div><body></html>';
|
||||
response.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
response.end(s);
|
||||
} else if (request.url.indexOf(GET_CARDS_ENDPOINT) === 0) {
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce' });
|
||||
getCards(node, request, response);
|
||||
} else if (request.url === '/reset') {
|
||||
node.resetAll();
|
||||
response.writeHead(302, { Location: '/' });
|
||||
response.end();
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end('404: Resource not found for method GET.');
|
||||
}
|
||||
} else if (request.method === 'POST') {
|
||||
if (request.url.indexOf(GET_CARDS_ENDPOINT) === 0) {
|
||||
let body = '';
|
||||
request.on('data', (chunk) => { body += chunk; });
|
||||
request.on('end', () => {
|
||||
const ignores = body.split(/\\n|\n/);
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce' });
|
||||
getCards(node, request, response, ignores);
|
||||
});
|
||||
} else if (request.url === POST_SENTENCES_ENDPOINT) {
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce' });
|
||||
postSentences(node, request, response);
|
||||
} else if (request.url === '/ui/sentences') {
|
||||
response.writeHead(302, { Location: '/' });
|
||||
postSentences(node, request, response);
|
||||
} else if (request.url === '/agent-name') {
|
||||
let body = '';
|
||||
request.on('data', (chunk) => { body += chunk; });
|
||||
request.on('end', () => {
|
||||
body = decodeURIComponent(body.replace('name=', '').replace(/\+/g, ' '));
|
||||
node.agent.setName(body);
|
||||
response.writeHead(302, { Location: '/' });
|
||||
response.end();
|
||||
});
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end('404: Resource not found for method POST.');
|
||||
}
|
||||
} else {
|
||||
response.writeHead(405);
|
||||
response.end('405: Method not allowed on this server.');
|
||||
}
|
||||
}).listen(port || 5555);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startServer();
|
||||
}
|
134
src/CardHandler.js
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
class CardHandler {
|
||||
|
||||
constructor(agent) {
|
||||
this.agent = agent;
|
||||
this.node = agent.node;
|
||||
this.handlers = {
|
||||
|
||||
'ask card': (card) => {
|
||||
// Get the relevant information from the node
|
||||
const data = this.node.askQuestion(card.content);
|
||||
for (const policy of this.node.getInstances('ask policy')) {
|
||||
if (policy.enabled === 'true' && policy.target && policy.target.name) {
|
||||
const targetName = policy.target.name;
|
||||
if (!(targetName in this.agent.policyHandler.unsentAskCards)) { this.agent.policyHandler.unsentAskCards[targetName] = []; }
|
||||
this.agent.policyHandler.unsentAskCards[targetName].push(card);
|
||||
}
|
||||
}
|
||||
|
||||
if (card.is_from) {
|
||||
// Prepare the response 'tell card' and add this back to the node
|
||||
let urls;
|
||||
let c;
|
||||
if (data.data) {
|
||||
urls = data.data.match(/(https?:\/\/[a-zA-Z0-9./\-+_&=?!%]*)/gi);
|
||||
c = `there is a ${data.type} card named 'msg_{uid}' that is from the agent '${this.agent.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has '${data.data.replace(/'/g, "\\'")}' as content`;
|
||||
} else {
|
||||
c = `there is a gist card named 'msg_{uid}' that is from the agent '${this.agent.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has 'Sorry; your question was not understood.' as content`;
|
||||
}
|
||||
|
||||
for (const from of card.is_froms) {
|
||||
c += ` and is to the ${from.type.name} '${from.name}'`;
|
||||
}
|
||||
if (urls) {
|
||||
for (const url of urls) {
|
||||
c += ` and has '${url}' as linked content`;
|
||||
}
|
||||
}
|
||||
c += ` and is in reply to the card '${card.name}'`;
|
||||
return this.node.addSentence(c);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
'tell card': (card) => {
|
||||
// Add the CE sentence to the node
|
||||
const data = this.node.addCE(card.content, card.is_from && card.is_from.name);
|
||||
|
||||
if (!data.success && card.is_from) {
|
||||
// If unsuccessful, write an error back
|
||||
return this.node.addSentence(`there is a gist card named 'msg_{uid}' that is from the agent '${this.agent.name.replace(/'/g, "\\'")}' and is to the ${card.is_from.type.name} '${card.is_from.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has 'Sorry. Your input was not understood.' as content and is in reply to the card '${card.name}'.`);
|
||||
}
|
||||
|
||||
if (data.success === true) {
|
||||
// Add sentence to any active tell policy queues
|
||||
for (const policy of this.node.getInstances('tell policy')) {
|
||||
if (policy.enabled === 'true' && policy.target && policy.target.name) {
|
||||
const targetName = policy.target.name;
|
||||
if (!(targetName in this.agent.policyHandler.unsentTellCards)) { this.agent.policyHandler.unsentTellCards[targetName] = []; }
|
||||
this.agent.policyHandler.unsentTellCards[targetName].push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (card.is_from) {
|
||||
// Check feedback policies to see if input 'tell card' requires a response
|
||||
for (const policy of this.node.getInstances('feedback policy')) {
|
||||
if (policy.enabled === 'true' && policy.target && policy.target.name) {
|
||||
const ack = policy.acknowledgement;
|
||||
if (policy.target.name.toLowerCase() === card.is_from.name.toLowerCase()) {
|
||||
let c;
|
||||
if (ack === 'basic') { c = 'OK.'; } else if (data.type === 'tell') {
|
||||
c = `OK. I added this to my knowledge base: ${data.data}`;
|
||||
} else if (data.type === 'ask' || data.type === 'confirm' || data.type === 'gist') {
|
||||
c = data.data;
|
||||
}
|
||||
return this.node.addSentence(`there is a ${data.type} card named 'msg_{uid}' that is from the agent '${this.agent.name.replace(/'/g, "\\'")}' and is to the ${card.is_from.type.name} '${card.is_from.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has '${c.replace(/'/g, "\\'")}' as content and is in reply to the card '${card.name}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
'nl card': (card) => {
|
||||
let data = this.node.addCE(card.content, card.is_from && card.is_from.name);
|
||||
// If valid CE, then replicate the nl card as a tell card ('autoconfirm')
|
||||
if (data.success) {
|
||||
return this.node.addSentence(`there is a tell card named 'msg_{uid}' that is from the ${card.is_from.type.name} '${card.is_from.name.replace(/'/g, "\\'")}' and is to the agent '${this.agent.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has '${card.content.replace(/'/g, "\\'")}' as content.`);
|
||||
}
|
||||
data = this.node.askQuestion(card.content);
|
||||
// If question was success replicate as ask card ('autoask')
|
||||
if (data.success) {
|
||||
return this.node.addSentence(`there is an ask card named 'msg_{uid}' that is from the ${card.is_from.type.name} '${card.is_from.name.replace(/'/g, "\\'")}' and is to the agent '${this.agent.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has '${card.content.replace(/'/g, "\\'")}' as content.`);
|
||||
}
|
||||
// If question not understood then place the response to the NL card in a new response
|
||||
data = this.node.addNL(card.content);
|
||||
return this.node.addSentence(`there is a ${data.type} card named 'msg_{uid}' that is from the agent '${this.agent.name.replace(/'/g, "\\'")}' and is to the ${card.is_from.type.name} '${card.is_from.name.replace(/'/g, "\\'")}' and has the timestamp '{now}' as timestamp and has '${data.data.replace(/'/g, "\\'")}' as content and is in reply to the card '${card.name}'.`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handle(card) {
|
||||
if (card.is_tos && card.content && this.agent.handledCards.indexOf(card.name) === -1) {
|
||||
// Determine whether or not to read or ignore this card:
|
||||
for (const to of card.is_tos) {
|
||||
if (to.name.toLowerCase() === this.agent.name.toLowerCase()) {
|
||||
this.handlers[card.type.name](card);
|
||||
this.agent.handledCards.push(card.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CardHandler;
|
226
src/NLParser.js
Normal file
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
class NLParser {
|
||||
|
||||
/*
|
||||
* Submit natural language to be processed by node.
|
||||
* This results in
|
||||
* - string representing what the node THINKS the input is trying to say.
|
||||
* (this could be returned as a confirm card
|
||||
* This method does not update the conceptual model.
|
||||
*
|
||||
* Returns: str
|
||||
*/
|
||||
parse(input) {
|
||||
const t = input.replace(/'/g, '').replace(/\./g, '');
|
||||
const tokens = t.split(' ');
|
||||
const andFacts = t.split(/\band\b/);
|
||||
|
||||
// Try to find any mentions of known instances and tie them together using
|
||||
// values and relationships.
|
||||
|
||||
const commonWords = ['there', 'what', 'who', 'where', 'theres', 'is', 'as', 'and', 'has', 'that', 'the', 'a', 'an', 'named', 'called', 'name', 'with', 'conceptualise', 'on', 'at', 'in'];
|
||||
let focusInstance = null;
|
||||
let smallestIndex = 999999;
|
||||
for (let i = 0; i < this.node.instances.length; i += 1) {
|
||||
const possibleNames = this.node.instances[i].synonyms.concat(this.node.instances[i].name);
|
||||
for (let j = 0; j < possibleNames.length; j += 1) {
|
||||
if (t.toLowerCase().indexOf(possibleNames[j].toLowerCase()) > -1) {
|
||||
if (t.toLowerCase().indexOf(possibleNames[j].toLowerCase()) < smallestIndex) {
|
||||
focusInstance = this.node.instances[i];
|
||||
smallestIndex = t.toLowerCase().indexOf(possibleNames[j].toLowerCase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (focusInstance) {
|
||||
const focusConcept = focusInstance.type;
|
||||
const focusInstanceWords = focusInstance.name.toLowerCase().split(' ');
|
||||
const focusConceptWords = focusConcept.name.toLowerCase().split(' ');
|
||||
for (let i = 0; i < focusInstanceWords.length; i += 1) { commonWords.push(focusInstanceWords[i]); }
|
||||
for (let i = 0; i < focusConceptWords.length; i += 1) { commonWords.push(focusConceptWords[i]); }
|
||||
|
||||
const ce = `the ${focusConcept.name} '${focusInstance.name}' `;
|
||||
const facts = [];
|
||||
|
||||
const parents = focusConcept.ancestors;
|
||||
parents.push(focusConcept);
|
||||
|
||||
let possibleRelationships = [];
|
||||
let possibleValues = [];
|
||||
for (let i = 0; i < parents.length; i += 1) {
|
||||
possibleRelationships = possibleRelationships.concat(parents[i].relationships);
|
||||
possibleValues = possibleValues.concat(parents[i].values);
|
||||
}
|
||||
|
||||
for (let k = 0; k < andFacts.length; k += 1) {
|
||||
const f = andFacts[k].toLowerCase();
|
||||
const factTokens = f.split(' ');
|
||||
for (let i = 0; i < possibleValues.length; i += 1) {
|
||||
const valueWords = possibleValues[i].label.toLowerCase().split(' ');
|
||||
for (let j = 0; j < valueWords.length; j += 1) { commonWords.push(valueWords[j]); }
|
||||
|
||||
if (possibleValues[i].concept) {
|
||||
const valueConcept = possibleValues[i].concept;
|
||||
const valueInstances = this.node.getInstances(valueConcept.name, true);
|
||||
for (let j = 0; j < valueInstances.length; j += 1) {
|
||||
const possibleNames = valueInstances[j].synonyms.concat(valueInstances[j].name);
|
||||
for (let l = 0; l < possibleNames.length; l += 1) {
|
||||
if (f.toLowerCase().indexOf(possibleNames[l].toLowerCase()) > -1) {
|
||||
facts.push(`has the ${valueConcept.name} '${valueInstances[j].name}' as ${possibleValues[i].label}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (f.toLowerCase().indexOf(possibleValues[i].label.toLowerCase()) > -1) {
|
||||
let valueName = '';
|
||||
for (let j = 0; j < factTokens.length; j += 1) {
|
||||
if (commonWords.indexOf(factTokens[j].toLowerCase()) === -1) {
|
||||
valueName += `${factTokens[j]} `;
|
||||
}
|
||||
}
|
||||
if (valueName !== '') {
|
||||
facts.push(`has '${valueName.trim()}' as ${possibleValues[i].label}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const usedIndices = [];
|
||||
for (let i = 0; i < possibleRelationships.length; i += 1) {
|
||||
if (possibleRelationships[i].concept) {
|
||||
const relConcept = possibleRelationships[i].concept;
|
||||
const relInstances = this.node.getInstances(relConcept.name, true);
|
||||
for (let j = 0; j < relInstances.length; j += 1) {
|
||||
const possibleNames = relInstances[j].synonyms.concat(relInstances[j].name);
|
||||
for (let l = 0; l < possibleNames.length; l += 1) {
|
||||
const index = f.toLowerCase().indexOf(` ${possibleNames[l].toLowerCase()}`); // ensure object at least starts with the phrase (but not ends with, as might be plural)
|
||||
if (index > -1 && usedIndices.indexOf(index) === -1) {
|
||||
facts.push(`${possibleRelationships[i].label} the ${relConcept.name} '${relInstances[j].name}'`);
|
||||
usedIndices.push(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (facts.length > 0) {
|
||||
return [true, ce + facts.join(' and ')];
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.node.concepts.length; i += 1) {
|
||||
if (t.toLowerCase().indexOf(this.node.concepts[i].name.toLowerCase()) > -1) {
|
||||
const conceptWords = this.node.concepts[i].name.toLowerCase().split(' ');
|
||||
commonWords.push(this.node.concepts[i].name.toLowerCase());
|
||||
for (let j = 0; j < conceptWords; j += 1) {
|
||||
commonWords.push(conceptWords[j]);
|
||||
}
|
||||
let newInstanceName = '';
|
||||
for (let j = 0; j < tokens.length; j += 1) {
|
||||
if (commonWords.indexOf(tokens[j].toLowerCase()) === -1) {
|
||||
newInstanceName += `${tokens[j]} `;
|
||||
}
|
||||
}
|
||||
if (newInstanceName && newInstanceName.length) {
|
||||
return [true, `there is a ${this.node.concepts[i].name} named '${newInstanceName.trim()}'`];
|
||||
}
|
||||
return [true, `there is a ${this.node.concepts[i].name} named '${this.node.concepts[i].name} ${this.node.instances.length}${1}'`];
|
||||
}
|
||||
}
|
||||
return [false, `Un-parseable input: ${t}`];
|
||||
}
|
||||
|
||||
/*
|
||||
* Return a string representing a guess at what the user is trying to say next.
|
||||
* Actually what is returned is the input string + the next word/phrase based on:
|
||||
* - current state of conceptual model (i.e. names/relationships of concepts/instances)
|
||||
* - key words/phrases (e.g. "conceptualise a ")
|
||||
*
|
||||
* Returns: str
|
||||
*/
|
||||
guessNext(t) {
|
||||
const s = t.trim().toLowerCase();
|
||||
const tokens = t.split(' ');
|
||||
let numberOfTildes = 0;
|
||||
let indexOfFirstTilde = 0;
|
||||
for (let i = 0; i < tokens.length; i += 1) { if (tokens[i] === '~') { numberOfTildes += 1; if (numberOfTildes === 1) { indexOfFirstTilde = i; } } }
|
||||
const possibleWords = [];
|
||||
if (t === '') { return t; }
|
||||
if (numberOfTildes === 1) {
|
||||
try {
|
||||
return `${t} ~ ${tokens[indexOfFirstTilde + 1].charAt(0).toUpperCase()} `;
|
||||
} catch (err) { /* continue anyway */ }
|
||||
}
|
||||
if (s.match(/^conceptualise a ~ (.*) ~ [A-Z] /)) {
|
||||
return `${t} that `;
|
||||
}
|
||||
|
||||
if (tokens.length < 2) {
|
||||
possibleWords.push('conceptualise a ~ ');
|
||||
possibleWords.push('there is a ');
|
||||
possibleWords.push('where is ');
|
||||
possibleWords.push('what is ');
|
||||
possibleWords.push('who is ');
|
||||
}
|
||||
if (tokens.length > 2) {
|
||||
possibleWords.push("named '");
|
||||
possibleWords.push('that ');
|
||||
possibleWords.push('is a ');
|
||||
possibleWords.push('and is ');
|
||||
possibleWords.push('and has the ');
|
||||
possibleWords.push('the ');
|
||||
}
|
||||
|
||||
const mentionedInstances = [];
|
||||
|
||||
if (s.indexOf('there is') === -1 || tokens.length === 1) {
|
||||
for (let i = 0; i < this.node.instances.length; i += 1) {
|
||||
possibleWords.push(this.node.instances[i].name);
|
||||
if (s.indexOf(this.node.instances[i].name.toLowerCase()) > -1) {
|
||||
mentionedInstances.push(this.node.instances[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.node.concepts.length; i += 1) {
|
||||
possibleWords.push(this.node.concepts[i].name);
|
||||
let conceptMentioned = false;
|
||||
for (let j = 0; j < mentionedInstances.length; j += 1) {
|
||||
if (mentionedInstances[j].conceptId === this.node.concepts[i].id) { conceptMentioned = true; break; }
|
||||
}
|
||||
if (s.indexOf(this.node.concepts[i].name.toLowerCase()) > -1 || conceptMentioned) {
|
||||
for (let j = 0; j < this.node.concepts[i].values.length; j += 1) { possibleWords.push(this.node.concepts[i].values[j].label); }
|
||||
for (let j = 0; j < this.node.concepts[i].relationships.length; j += 1) { possibleWords.push(this.node.concepts[i].relationships[j].label); }
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < possibleWords.length; i += 1) {
|
||||
if (possibleWords[i].toLowerCase().indexOf(tokens[tokens.length - 1].toLowerCase()) === 0) {
|
||||
tokens[tokens.length - 1] = possibleWords[i];
|
||||
return tokens.join(' ');
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
module.exports = NLParser;
|
218
src/PolicyHandler.js
Normal file
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const POST_SENTENCES_ENDPOINT = '/sentences';
|
||||
const GET_CARDS_ENDPOINT = '/cards';
|
||||
|
||||
/*
|
||||
* Utility object to support network tasks.
|
||||
*/
|
||||
const net = {
|
||||
makeRequest(method, nodeURL, path, data, callback) {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.document) {
|
||||
net.makeRequestClient(method, nodeURL, path, data, callback);
|
||||
} else {
|
||||
net.makeRequestNode(method, nodeURL, path, data, callback);
|
||||
}
|
||||
} catch (err) { /* Continue even if network error */ }
|
||||
},
|
||||
makeRequestClient(method, nodeURL, path, data, callback) {
|
||||
const url = nodeURL.indexOf('http://') === -1 ? `http://${nodeURL}` : nodeURL;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url + path);
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 302) && callback) {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
};
|
||||
if (data) {
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(data);
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
},
|
||||
makeRequestNode(method, nodeURL, path, data, callback) {
|
||||
const portMatch = nodeURL.match(/.*:([0-9]*)/);
|
||||
let url = nodeURL;
|
||||
if (portMatch) {
|
||||
url = nodeURL.replace(/:[0-9]*/, '');
|
||||
}
|
||||
|
||||
const options = {
|
||||
hostname: url,
|
||||
path,
|
||||
port: portMatch ? parseInt(portMatch[1], 10) : 80,
|
||||
method: 'POST',
|
||||
};
|
||||
if (method === 'POST') {
|
||||
const request = require('http').request(options, (res) => {
|
||||
let response = '';
|
||||
res.on('data', (chunk) => { response += chunk; });
|
||||
res.on('end', () => {
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on('error', () => { /* continue anyway */ });
|
||||
request.write(data);
|
||||
request.end();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
class PolicyHandler {
|
||||
|
||||
constructor(agent) {
|
||||
this.agent = agent;
|
||||
this.node = agent.node;
|
||||
this.unsentTellCards = [];
|
||||
this.unsentAskCards = [];
|
||||
this.lastSuccessfulRequest = 0;
|
||||
this.handlers = {
|
||||
|
||||
'tell policy': (policy) => {
|
||||
// For each tell policy in place, send all currently-untold cards to each target
|
||||
// multiple cards to be sent to one target line-separated
|
||||
if (policy.target && policy.target.name && policy.target.address) {
|
||||
let data = '';
|
||||
for (const card of this.unsentTellCards[policy.target.name]) {
|
||||
if (card.is_tos && card.is_from.name.toLowerCase() !== policy.target.name.toLowerCase()) { // Don't send back a card sent from target agent
|
||||
// Make sure target is not already a recipient
|
||||
let inCard = false;
|
||||
for (const to of card.is_tos) {
|
||||
if (to.id === policy.target.id) { inCard = true; break; }
|
||||
}
|
||||
if (!inCard) {
|
||||
card.addRelationship('is to', policy.target);
|
||||
}
|
||||
data += `${card.ce}\n`;
|
||||
}
|
||||
}
|
||||
if (data.length) {
|
||||
net.makeRequest('POST', policy.target.address, POST_SENTENCES_ENDPOINT, data, () => {
|
||||
this.lastSuccessfulRequest = new Date().getTime();
|
||||
this.unsentTellCards[policy.target.name] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ask policy': (policy) => {
|
||||
// For each ask policy in place send all currently-untold cards to each target
|
||||
// multiple cards to be sent to one target are line-separated
|
||||
if (policy.target && policy.target.name) {
|
||||
let data = '';
|
||||
for (const card of this.unsentAskCards[policy.target.name]) {
|
||||
const froms = card.is_froms;
|
||||
const tos = card.is_tos;
|
||||
if (tos && card.is_from && card.is_from.name.toLowerCase() !== policy.target.name.toLowerCase()) { // Don't send back a card sent from target agent
|
||||
// Make sure target is not already a recipient
|
||||
let inCard = false;
|
||||
for (const to of tos) {
|
||||
if (to.id === policy.target.id) { inCard = true; break; }
|
||||
}
|
||||
if (!inCard) {
|
||||
card.addRelationship('is to', policy.target);
|
||||
}
|
||||
// Make sure an agent is not already a sender
|
||||
inCard = false;
|
||||
for (const from of froms) {
|
||||
if (from.id === this.agent.getInstance().id) { inCard = true; break; }
|
||||
}
|
||||
if (!inCard) {
|
||||
card.addRelationship('is from', this.agent.getInstance());
|
||||
}
|
||||
data += `${card.ce}\n`;
|
||||
}
|
||||
}
|
||||
if (data.length) {
|
||||
net.makeRequest('POST', policy.target.address, POST_SENTENCES_ENDPOINT, data, () => {
|
||||
this.lastSuccessfulRequest = new Date().getTime();
|
||||
this.unsentAskCards[policy.target.name] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'listen policy': (policy) => {
|
||||
// Make request to target to get cards addressed to THIS agent
|
||||
if (policy.target && policy.target.address) {
|
||||
// Build ignore list of already-processed cards:
|
||||
let data = '';
|
||||
for (const card of this.node.getInstances('card', true)) {
|
||||
data = `${data + card.name}\n`;
|
||||
}
|
||||
net.makeRequest('POST', policy.target.address, `${GET_CARDS_ENDPOINT}?agent=${this.agent.name}`, data, (res) => {
|
||||
this.lastSuccessfulRequest = new Date().getTime();
|
||||
this.node.addSentences(res.split('\n'));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'forwardall policy': (policy) => {
|
||||
// Forward any cards sent to THIS agent to every other known agent
|
||||
const agents = policy.all_agents === 'true' ? this.node.getInstances('agent') : policy.targets;
|
||||
const cards = this.node.getInstances('tell card');
|
||||
if (policy.start_time) {
|
||||
const startTime = policy.start_time;
|
||||
for (const card of cards) {
|
||||
let toAgent = false;
|
||||
const tos = card.is_tos;
|
||||
const from = card.is_froms[0];
|
||||
const cardTimestamp = card.timestamp.name;
|
||||
if (tos && parseInt(cardTimestamp, 10) > parseInt(startTime, 10)) {
|
||||
for (const to of tos) {
|
||||
if (to.name === this.agent.name) { // If card sent to THIS agent
|
||||
toAgent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toAgent) {
|
||||
// Add each other agent as a recipient (if they aren't already)
|
||||
for (const agentCheck of agents) {
|
||||
let agentIsRecipient = false;
|
||||
for (const to of tos) {
|
||||
if (to.name.toLowerCase() === agentCheck.name.toLowerCase()) {
|
||||
agentIsRecipient = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!agentIsRecipient && agentCheck.name.toLowerCase() !== this.agent.name.toLowerCase() && agentCheck.name.toLowerCase() !== from.name.toLowerCase()) {
|
||||
card.addRelationship('is to', agentCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handle(policy) {
|
||||
if (policy.enabled === 'true') {
|
||||
this.handlers[policy.type.name](policy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PolicyHandler;
|
392
src/QuestionParser.js
Normal file
@ -0,0 +1,392 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
class QuestionParser {
|
||||
|
||||
/*
|
||||
* Submit a who/what/where question to be processed by node.
|
||||
* This may result in
|
||||
* - a response to the question returned
|
||||
* - error returned (i.e. invalid question)
|
||||
* This method does not update the conceptual model.
|
||||
*
|
||||
* Returns: [bool, str] (bool = success, str = error or response)
|
||||
*/
|
||||
parse(t) {
|
||||
const input = t.trim();
|
||||
if (t.match(/^where (is|are)/i)) {
|
||||
return this.whereIs(input);
|
||||
} else if (t.match(/^(\bwho\b|\bwhat\b) is(?: \bin?\b | \bon\b | \bat\b)/i)) {
|
||||
return this.whatIsIn(input);
|
||||
} else if (t.match(/^(\bwho\b|\bwhat\b) (?:is|are)/i)) {
|
||||
return this.whatIs(input);
|
||||
} else if (t.match(/^(\bwho\b|\bwhat\b) does/i)) {
|
||||
return this.whatDoes(input);
|
||||
} else if (t.match(/^(\bwho\b|\bwhat\b)/i)) {
|
||||
return this.whatRelationship(input);
|
||||
} else if (t.match(/^list (\ball\b|\binstances\b)/i)) {
|
||||
return this.listInstances(input);
|
||||
}
|
||||
return [false, null];
|
||||
}
|
||||
|
||||
whereIs(t) {
|
||||
const thing = t.match(/^where (?:is|are)(?: \ban?\b | \bthe\b | )([a-zA-Z0-9 ]*)/i)[1].replace(/\?/g, '');
|
||||
const instance = this.node.getInstanceByName(thing);
|
||||
let message;
|
||||
if (!instance) {
|
||||
message = `I don't know what ${thing} is.`;
|
||||
return [true, message];
|
||||
}
|
||||
const locatableInstances = this.node.getInstances('location', true);
|
||||
const locatableIds = [];
|
||||
const places = {};
|
||||
let placeFound = false;
|
||||
for (const locatableInstance of locatableInstances) { locatableIds.push(locatableInstance.id); }
|
||||
|
||||
for (const value of instance.values) {
|
||||
if (locatableIds.indexOf(value.instance.id) > -1) {
|
||||
const place = `has ${value.instance.name} as ${value.label}`;
|
||||
if (!(place in places)) {
|
||||
places[place] = 0;
|
||||
}
|
||||
places[place] += 1;
|
||||
placeFound = true;
|
||||
}
|
||||
}
|
||||
for (const relationship of instance.relationships) {
|
||||
if (locatableIds.indexOf(relationship.instance.id) > -1) {
|
||||
const place = `${relationship.label} ${relationship.instance.name}`;
|
||||
if (!(place in places)) {
|
||||
places[place] = 0;
|
||||
}
|
||||
places[place] += 1;
|
||||
placeFound = true;
|
||||
}
|
||||
}
|
||||
if (!placeFound) {
|
||||
message = `I don't know where ${instance.name} is.`;
|
||||
return [true, message];
|
||||
}
|
||||
message = instance.name;
|
||||
for (const place in places) {
|
||||
if (place) {
|
||||
message += ` ${place}`;
|
||||
if (places[place] > 1) {
|
||||
message += ` (${places[place]} times)`;
|
||||
}
|
||||
message += ' and';
|
||||
}
|
||||
}
|
||||
return [true, `${message.substring(0, message.length - 4)}.`];
|
||||
}
|
||||
|
||||
whatIsIn(t) {
|
||||
const thing = t.match(/^(?:\bwho\b|\bwhat\b) is(?: \bin?\b | \bon\b | \bat\b)([a-zA-Z0-9 ]*)/i)[1].replace(/\?/g, '').replace(/\bthe\b/g, '').replace(/'/g, '');
|
||||
let instance = null;
|
||||
const locatableInstances = this.node.getInstances('location', true);
|
||||
for (const locatableInstance of locatableInstances) {
|
||||
if (thing.toLowerCase().indexOf(locatableInstance.name.toLowerCase()) > -1) {
|
||||
instance = locatableInstance; break;
|
||||
}
|
||||
}
|
||||
if (!instance) {
|
||||
return [true, `${thing} is not an instance of type location.`];
|
||||
}
|
||||
const things = {};
|
||||
let thingFound = false;
|
||||
for (const checkInstance of this.node.instances) {
|
||||
for (const value of checkInstance.values) {
|
||||
if (value.instance.id === instance.id) {
|
||||
const thing2 = `the ${checkInstance.type.name} ${checkInstance.name} has the ${instance.type.name} ${instance.name} as ${value.label}`;
|
||||
if (!(thing2 in things)) {
|
||||
things[thing2] = 0;
|
||||
}
|
||||
things[thing2] += 1;
|
||||
thingFound = true;
|
||||
}
|
||||
}
|
||||
for (const relationship of checkInstance.relationships) {
|
||||
if (relationship.instance.id === instance.id) {
|
||||
const thing2 = `the ${checkInstance.type.name} ${checkInstance.name} ${relationship.label} the ${instance.type.name} ${instance.name}`;
|
||||
if (!(thing2 in things)) {
|
||||
things[thing2] = 0;
|
||||
}
|
||||
things[thing2] += 1;
|
||||
thingFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!thingFound) {
|
||||
return [true, `I don't know what is located in/on/at the ${instance.type.name} ${instance.name}.`];
|
||||
}
|
||||
|
||||
let message = '';
|
||||
for (const thing2 in things) {
|
||||
message += ` ${thing2}`;
|
||||
if (things[thing] > 1) {
|
||||
message += ` (${things[thing2]} times)`;
|
||||
}
|
||||
message += ' and';
|
||||
}
|
||||
return [true, `${message.substring(0, message.length - 4)}.`];
|
||||
}
|
||||
|
||||
whatIs(input) {
|
||||
const t = input.replace(/\?/g, '').replace(/'/g, '').replace(/\./g, '');
|
||||
|
||||
// If we have an exact match (i.e. 'who is The Doctor?')
|
||||
let name = t.match(/^(\bwho\b|\bwhat\b) (?:is|are) ([a-zA-Z0-9_ ]*)/i);
|
||||
let instance;
|
||||
if (name) {
|
||||
instance = this.node.getInstanceByName(name[2]);
|
||||
if (instance) {
|
||||
return [true, instance.gist];
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, try and infer it
|
||||
name = t.match(/^(?:\bwho\b|\bwhat\b) (?:is|are)(?: \ban?\b | \bthe\b | )([a-zA-Z0-9_ ]*)/i)[1].replace(/\?/g, '').replace(/'/g, '');
|
||||
instance = this.node.getInstanceByName(name);
|
||||
if (!instance) {
|
||||
const concept = this.node.getConceptByName(name);
|
||||
if (!concept) {
|
||||
const possibilities = [];
|
||||
for (const checkConcept of this.node.concepts) {
|
||||
for (const checkValue of checkConcept.values) {
|
||||
const v = checkValue;
|
||||
if (v.label.toLowerCase() === name.toLowerCase()) {
|
||||
if (!v.concept) {
|
||||
possibilities.push(`is a possible value of a type of ${checkConcept.name} (e.g. "the ${checkConcept.name} '${checkConcept.name.toUpperCase()} NAME' has 'VALUE' as ${name}")`);
|
||||
} else {
|
||||
possibilities.push(`is a possible ${v.concept.name} type of a type of ${checkConcept.name} (e.g. "the ${checkConcept.name} '${checkConcept.name.toUpperCase()} NAME' has the ${v.concept.name} '${v.concept.name.toUpperCase()} NAME' as ${name}")`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const checkRelationship of checkConcept.relationships) {
|
||||
const r = checkRelationship;
|
||||
if (r.label.toLowerCase() === name.toLowerCase()) {
|
||||
possibilities.push(`describes the relationship between a type of ${checkConcept.name} and a type of ${r.concept.name} (e.g. "the ${checkConcept.name} '${checkConcept.name.toUpperCase()} NAME' ${name} the ${r.concept.name} '${r.concept.name.toUpperCase()} NAME'")`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (possibilities.length > 0) {
|
||||
return [true, `'${name}' ${possibilities.join(' and ')}.`];
|
||||
}
|
||||
|
||||
// If nothing found, do fuzzy search
|
||||
const searchReturn = this.fuzzySearch(t);
|
||||
let fuzzyGist = 'I know about ';
|
||||
let fuzzyFound = false;
|
||||
for (const key in searchReturn) {
|
||||
if (searchReturn[key].length > 1) {
|
||||
for (let i = 0; i < searchReturn[key].length; i += 1) {
|
||||
if (i === 0) {
|
||||
if (!fuzzyFound) {
|
||||
fuzzyGist += `the ${key}s '${searchReturn[key][i]}'`;
|
||||
} else {
|
||||
fuzzyGist += `The ${key}s '${searchReturn[key][i]}'`;
|
||||
}
|
||||
} else {
|
||||
fuzzyGist += `, '${searchReturn[key][i]}'`;
|
||||
}
|
||||
if (i === searchReturn[key].length - 1) {
|
||||
fuzzyGist += '. ';
|
||||
}
|
||||
}
|
||||
} else if (!fuzzyFound) {
|
||||
fuzzyGist += `the ${key} '${searchReturn[key][0]}'. `;
|
||||
} else {
|
||||
fuzzyGist += `The ${key} '${searchReturn[key][0]}'. `;
|
||||
}
|
||||
fuzzyFound = true;
|
||||
}
|
||||
if (fuzzyFound) {
|
||||
return [true, fuzzyGist];
|
||||
}
|
||||
return [true, 'I don\'t know who or what that is.'];
|
||||
}
|
||||
return [true, concept.gist];
|
||||
}
|
||||
return [true, instance.gist];
|
||||
}
|
||||
|
||||
whatDoes(t) {
|
||||
try {
|
||||
const data = t.match(/^(\bwho\b|\bwhat\b) does ([a-zA-Z0-9_ ]*)/i);
|
||||
const body = data[2].replace(/\ban\b/gi, '').replace(/\bthe\b/gi, '').replace(/\ba\b/gi, '');
|
||||
const tokens = body.split(' ');
|
||||
let instance;
|
||||
for (let i = 0; i < tokens.length; i += 1) {
|
||||
const testString = tokens.slice(0, i).join(' ').trim();
|
||||
if (!instance) {
|
||||
instance = this.node.getInstanceByName(testString);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (instance) {
|
||||
const propertyName = tokens.splice(instance.name.split(' ').length, tokens.length - 1).join(' ').trim();
|
||||
let fixedPropertyName = propertyName;
|
||||
let property = instance.property(propertyName);
|
||||
if (!property) {
|
||||
fixedPropertyName = propertyName.replace(/s/ig, '');
|
||||
property = instance.property(fixedPropertyName);
|
||||
}
|
||||
if (!property) {
|
||||
const propTokens = propertyName.split(' ');
|
||||
propTokens[0] = `${propTokens[0]}s`;
|
||||
fixedPropertyName = propTokens.join(' ').trim();
|
||||
property = instance.property(fixedPropertyName);
|
||||
}
|
||||
if (property) {
|
||||
return [true, `${instance.name} ${fixedPropertyName} the ${property.type.name} ${property.name}.`];
|
||||
}
|
||||
return [true, `Sorry - I don't know that property about the ${instance.type.name} ${instance.name}.`];
|
||||
}
|
||||
} catch (err) {
|
||||
return [false, 'Sorry - I can\'t work out what you\'re asking.'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
whatRelationship(t) {
|
||||
const data = t.match(/^(\bwho\b|\bwhat\b) ([a-zA-Z0-9_ ]*)/i);
|
||||
const body = data[2].replace(/\ban\b/gi, '').replace(/\bthe\b/gi, '').replace(/\ba\b/gi, '');
|
||||
const tokens = body.split(' ');
|
||||
let instance;
|
||||
for (let i = 0; i < tokens.length; i += 1) {
|
||||
const testString = tokens.slice(tokens.length - (i + 1), tokens.length).join(' ').trim();
|
||||
if (!instance) {
|
||||
instance = this.node.getInstanceByName(testString);
|
||||
}
|
||||
if (!instance && testString[testString.length - 1].toLowerCase() === 's') {
|
||||
instance = this.node.getInstanceByName(testString.substring(0, testString.length - 1));
|
||||
}
|
||||
if (instance) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (instance) {
|
||||
const propertyName = tokens.splice(0, tokens.length - instance.name.split(' ').length).join(' ').trim();
|
||||
for (let i = 0; i < this.node.instances.length; i += 1) {
|
||||
const subject = this.node.instances[i];
|
||||
let fixedPropertyName = propertyName;
|
||||
let property = subject.property(propertyName);
|
||||
if (!property) {
|
||||
const propTokens = propertyName.split(' ');
|
||||
if (propTokens[0][propTokens[0].length - 1].toLowerCase() === 's') {
|
||||
propTokens[0] = propTokens[0].substring(0, propTokens[0].length - 1);
|
||||
}
|
||||
fixedPropertyName = propTokens.join(' ').trim();
|
||||
property = subject.property(fixedPropertyName);
|
||||
}
|
||||
if (!property) {
|
||||
const propTokens = propertyName.split(' ');
|
||||
propTokens[0] = `${propTokens[0]}s`;
|
||||
fixedPropertyName = propTokens.join(' ').trim();
|
||||
property = subject.property(fixedPropertyName);
|
||||
}
|
||||
if (property && property.name === instance.name) {
|
||||
return [true, `${subject.name} ${fixedPropertyName} the ${property.type.name} ${property.name}.`];
|
||||
}
|
||||
}
|
||||
return [true, `Sorry - I don't know that property about the ${instance.type.name} ${instance.name}.`];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
listInstances(t) {
|
||||
let ins = [];
|
||||
let s = '';
|
||||
if (t.toLowerCase().indexOf('list instances of type') === 0) {
|
||||
const con = t.toLowerCase().replace('list instances of type', '').trim();
|
||||
ins = this.node.getInstances(con);
|
||||
s = `Instances of type '${con}':`;
|
||||
} else if (t.toLowerCase().indexOf('list all instances of type') === 0) {
|
||||
const con = t.toLowerCase().replace('list all instances of type', '').trim();
|
||||
ins = this.node.getInstances(con, true);
|
||||
s = `All instances of type '${con}':`;
|
||||
} else if (t.toLowerCase() === 'list instances') {
|
||||
ins = this.node.instances;
|
||||
s = 'All instances:';
|
||||
}
|
||||
if (ins.length === 0) {
|
||||
return [true, 'I could not find any instances matching your query.'];
|
||||
}
|
||||
const names = [];
|
||||
for (let i = 0; i < ins.length; i += 1) {
|
||||
names.push(ins[i].name);
|
||||
}
|
||||
return [true, `${s} ${names.join(', ')}`];
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* Search the knowledge base for an instance name similar to the one asked about.
|
||||
*/
|
||||
fuzzySearch(sentence) {
|
||||
const searchFor = sentence.match(/^(?:\bwho\b|\bwhat\b) (?:is|are)(?: \ban?\b | \bthe\b | )([a-zA-Z0-9_ ]*)/i)[1].replace(/\?/g, '').replace(/'/g, '');
|
||||
const instances = this.node.getInstances();
|
||||
let multipleSearch;
|
||||
let instancesFiltered = [];
|
||||
|
||||
if (searchFor.indexOf(' ')) {
|
||||
// if theres spaces then split
|
||||
multipleSearch = searchFor.split(' ');
|
||||
}
|
||||
|
||||
if (multipleSearch) {
|
||||
// loop through to create return string
|
||||
for (let x = 0; x < multipleSearch.length; x += 1) {
|
||||
const instancesFilteredTemp = instances.filter((input) => {
|
||||
if (input.name.toUpperCase().includes(multipleSearch[x].toUpperCase())) {
|
||||
return input;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
instancesFiltered = instancesFiltered.concat(instancesFilteredTemp);
|
||||
}
|
||||
} else {
|
||||
// single search term
|
||||
instancesFiltered = instances.filter((input) => {
|
||||
if (input.name.toUpperCase().includes(searchFor.toUpperCase())) {
|
||||
return input;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
const instancesSummary = instancesFiltered.reduce((previous, current) => {
|
||||
const prev = previous;
|
||||
if (!prev[current.type.name]) {
|
||||
prev[current.type.name] = [];
|
||||
}
|
||||
prev[current.type.name].push(current.name);
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return instancesSummary;
|
||||
}
|
||||
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
module.exports = QuestionParser;
|
93
src/RuleEngine.js
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2017 W.M. Webberley & A.D. Preece (Cardiff University)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
class RuleEngine {
|
||||
|
||||
static parseRule(instruction) {
|
||||
if (!instruction) { return null; }
|
||||
const rule = {};
|
||||
let thenString = null;
|
||||
const relFacts = instruction.match(/^if the ([a-zA-Z0-9 ]*) ([A-Z]) ~ (.*) ~ the ([a-zA-Z0-9 ]*) ([A-Z]) then the (.*)/i);
|
||||
const valFacts = instruction.match(/^if the ([a-zA-Z0-9 ]*) ([A-Z]) has the ([a-zA-Z0-9 ]*) ([A-Z]) as ~ (.*) ~ then the (.*)/i);
|
||||
if (relFacts) {
|
||||
rule.if = {};
|
||||
rule.if.concept = relFacts[1];
|
||||
rule.if.relationship = {};
|
||||
rule.if.relationship.type = relFacts[4];
|
||||
rule.if.relationship.label = relFacts[3];
|
||||
thenString = relFacts[6];
|
||||
} else if (valFacts) {
|
||||
rule.if = {};
|
||||
rule.if.concept = valFacts[1];
|
||||
rule.if.value = {};
|
||||
rule.if.value.type = valFacts[3];
|
||||
rule.if.value.label = valFacts[5];
|
||||
thenString = valFacts[6];
|
||||
}
|
||||
|
||||
if (thenString) {
|
||||
const thenRelFacts = thenString.match(/^([a-zA-Z0-9 ]*) ([A-Z]) ~ (.*) ~ the ([a-zA-Z0-9 ]*) ([A-Z])/i);
|
||||
const thenValFacts = thenString.match(/^([a-zA-Z0-9 ]*) ([A-Z]) has the ([a-zA-Z0-9 ]*) ([A-Z]) as ~ (.*) ~/i);
|
||||
if (thenRelFacts) {
|
||||
rule.then = {};
|
||||
rule.then.concept = thenRelFacts[1];
|
||||
rule.then.relationship = {};
|
||||
rule.then.relationship.type = thenRelFacts[4];
|
||||
rule.then.relationship.label = thenRelFacts[3];
|
||||
} else if (thenValFacts) {
|
||||
rule.then = {};
|
||||
rule.then.concept = thenValFacts[1];
|
||||
rule.then.value = {};
|
||||
rule.then.value.type = thenValFacts[3];
|
||||
rule.then.value.label = thenValFacts[5];
|
||||
}
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
enactRules(subjectInstance, propertyType, objectInstance, source) {
|
||||
if (typeof objectInstance === 'string') {
|
||||
return;
|
||||
}
|
||||
const rules = this.node.getInstances('rule');
|
||||
for (let i = 0; i < rules.length; i += 1) {
|
||||
const rule = RuleEngine.parseRule(rules[i].instruction);
|
||||
if (!rule) { return; }
|
||||
if (rule.if.concept === subjectInstance.type.name) {
|
||||
if ((propertyType === 'relationship' && rule.if.relationship) || (propertyType === 'value' && rule.if.value)) {
|
||||
const ancestorConcepts = objectInstance.type.ancestors;
|
||||
ancestorConcepts.push(objectInstance.type);
|
||||
for (let j = 0; j < ancestorConcepts.length; j += 1) {
|
||||
if (ancestorConcepts[j].name.toLowerCase() === rule.if[propertyType].type.toLowerCase()) {
|
||||
if (rule.then.relationship && rule.then.relationship.type === subjectInstance.type.name) {
|
||||
objectInstance.addRelationship(rule.then.relationship.label, subjectInstance, false, source);
|
||||
} else if (rule.then.value && rule.then.value.type === subjectInstance.type.name) {
|
||||
objectInstance.addValue(rule.then.value.label, subjectInstance, false, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
module.exports = RuleEngine;
|
164
test/CEParser.js
Normal file
@ -0,0 +1,164 @@
|
||||
const CENode = require('../src/CENode.js');
|
||||
const CEModels = require('../models/index.js');
|
||||
const expect = require('expect.js');
|
||||
|
||||
let node;
|
||||
|
||||
describe('CEParser', function() {
|
||||
|
||||
describe('Conceptualisation', function (){
|
||||
before(function() {
|
||||
node = new CENode();
|
||||
});
|
||||
it('allow new concepts to be created', () => {
|
||||
node.addCE('conceptualise a ~ plant ~ P');
|
||||
expect(node.concepts.plant.name).to.be('plant');
|
||||
});
|
||||
it('allow for hierarchy of concepts', () => {
|
||||
node.addCE('conceptualise a ~ flower ~ F that is a plant');
|
||||
expect(node.concepts.flower.parents.length).to.be(1);
|
||||
expect(node.concepts.flower.parents[0].name).to.be('plant');
|
||||
});
|
||||
it('allow concepts to have multiple parents', () => {
|
||||
node.addCE('conceptualise a ~ dandilion ~ D that is a plant and is a flower');
|
||||
expect(node.concepts.dandilion.parents.length).to.be(2);
|
||||
});
|
||||
it('allow for multi-character variable names', () => {
|
||||
node.addCE('conceptualise a ~ seed ~ S1 that ~ grows into ~ the flower S2');
|
||||
expect(node.concepts.seed).to.not.be(undefined);
|
||||
expect(node.concepts.seed.relationships.length).to.be(1);
|
||||
expect(node.concepts.seed.relationships[0].label).to.be('grows into');
|
||||
});
|
||||
it('allow concepts to be instantiated with values', () => {
|
||||
node.addCE('conceptualise a ~ tree ~ T that has the value H as ~ height ~');
|
||||
expect(node.concepts.tree.values.length).to.be(1);
|
||||
expect(node.concepts.tree.values[0].label).to.be('height');
|
||||
});
|
||||
it('allow concepts to be modified with values', () => {
|
||||
node.addCE('conceptualise the tree T has the value A as ~ age ~');
|
||||
expect(node.concepts.tree.values.length).to.be(2);
|
||||
expect(node.concepts.tree.values[1].label).to.be('age');
|
||||
});
|
||||
it('allow concepts to be modified with relationships', () => {
|
||||
node.addCE('conceptualise the plant P ~ grows into ~ the tree T');
|
||||
expect(node.concepts.plant.relationships.length).to.be(1);
|
||||
expect(node.concepts.plant.relationships[0].label).to.be('grows into');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Instantiation', function() {
|
||||
before(function() {
|
||||
node = new CENode(CEModels.core, CEModels.test);
|
||||
});
|
||||
it('allow instances of known types to be created', function() {
|
||||
node.addCE('there is a person named Fred');
|
||||
expect(node.instances.fred.name).to.be('Fred');
|
||||
});
|
||||
it('prevent instances of unknown types from be created', function() {
|
||||
node.addCE('there is a man named Gerald');
|
||||
expect(node.instances.gerald).to.be(undefined);
|
||||
});
|
||||
it('allow instances to have relationships with other instances of the same type', () => {
|
||||
node.addCE('there is a person named Jane');
|
||||
node.addCE('the person Fred is married to the person Jane');
|
||||
expect(node.instances.fred.is_married_to.name).to.be('Jane');
|
||||
});
|
||||
it('allow instances to have relationships with other instances of different type', () => {
|
||||
node.addCE('there is a company named IBM');
|
||||
node.addCE('the person Fred works for the company IBM');
|
||||
expect(node.instances.fred.works_for.name).to.be('IBM');
|
||||
});
|
||||
it('allow complex modification sentences with quoted values', () => {
|
||||
node.addCE('the person \'Jane\' is married to the person \'Fred\' and works for the company \'IBM\' and has \'53\' as age');
|
||||
expect(node.instances.jane.is_married_to.name).to.be('Fred');
|
||||
expect(node.instances.jane.works_for.name).to.be('IBM');
|
||||
expect(node.instances.jane.age).to.be('53');
|
||||
});
|
||||
it('allow complex modification sentences with unquoted single-word values', () => {
|
||||
node.addCE('there is a person named Harry');
|
||||
node.addCE('there is a person named Harriet');
|
||||
node.addCE('there is a company named NatWest');
|
||||
node.addCE('the person Harry is married to the person Harriet and works for the company NatWest and has 53 as age');
|
||||
expect(node.instances.harry.is_married_to.name).to.be('Harriet');
|
||||
expect(node.instances.harry.works_for.name).to.be('NatWest');
|
||||
expect(node.instances.harry.age).to.be('53');
|
||||
});
|
||||
it('allow multiple concept types', () => {
|
||||
node.addCE('the person Jane is a barrister and a londoner');
|
||||
expect(node.instances.jane.subConcepts.length).to.be(2);
|
||||
expect(node.instances.jane.subConcepts[0].name).to.be('barrister');
|
||||
expect(node.instances.jane.subConcepts[1].name).to.be('londoner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Specific Examples', function() {
|
||||
beforeEach(function() {
|
||||
node = new CENode();
|
||||
});
|
||||
|
||||
it('there is a person named Fred.', function() {
|
||||
node.addCE('conceptualise a ~ person ~ P');
|
||||
node.addCE('there is a person named Fred');
|
||||
expect(node.instances.fred.name).to.be('Fred');
|
||||
});
|
||||
|
||||
it('the person Fred is married to the person Jane.', function() {
|
||||
node.addCE('conceptualise a ~ person ~ P that ~ is married to ~ the person Q');
|
||||
node.addCE('there is a person named Fred');
|
||||
node.addCE('the person Fred is married to the person Jane.');
|
||||
expect(node.instances.fred.is_married_to.name).to.be('Jane');
|
||||
});
|
||||
|
||||
it('the person Fred works for the company IBM.', function() {
|
||||
node.addCE('conceptualise a ~ company ~ C');
|
||||
node.addCE('conceptualise a ~ person ~ P that ~ works for ~ the company C');
|
||||
node.addCE('there is a person named Fred');
|
||||
node.addCE('the person Fred works for the company IBM.');
|
||||
expect(node.instances.fred.works_for.name).to.be('IBM');
|
||||
});
|
||||
|
||||
it('the person Fred works for the company IBM and is married to the person Jane and has 53 as age.', function() {
|
||||
node.addCE('conceptualise a ~ company ~ C');
|
||||
node.addCE('conceptualise a ~ person ~ P that ~ works for ~ the company C and ~ is married to ~ the person Q and has the value V as ~ age ~');
|
||||
node.addCE('there is a person named Fred');
|
||||
node.addCE('the person Fred works for the company IBM and is married to the person Jane and has 53 as age.');
|
||||
expect(node.instances.fred.works_for.name).to.be('IBM');
|
||||
expect(node.instances.fred.is_married_to.name).to.be('Jane');
|
||||
expect(node.instances.fred.age).to.be('53');
|
||||
});
|
||||
|
||||
it('the person Jane is a barrister and a londoner.', function() {
|
||||
node.addCE('conceptualise a ~ barrister ~ B');
|
||||
node.addCE('conceptualise a ~ londoner ~ L');
|
||||
node.addCE('conceptualise a ~ person ~ P');
|
||||
node.addCE('there is a person named Jane');
|
||||
node.addCE('the person Jane is a barrister and a londoner.');
|
||||
expect(node.instances.jane.subConcepts.length).to.be(2);
|
||||
expect(node.instances.jane.subConcepts[0].name).to.be('barrister');
|
||||
expect(node.instances.jane.subConcepts[1].name).to.be('londoner');
|
||||
});
|
||||
|
||||
it('conceptualise a ~ person ~ P.', function() {
|
||||
node.addCE('conceptualise a ~ person ~ P.');
|
||||
expect(node.concepts.person.name).to.be('person');
|
||||
});
|
||||
|
||||
it('conceptualise a ~ person ~ P1 that ~ is married to ~ the person P2.', function() {
|
||||
node.addCE('conceptualise a ~ person ~ P1 that ~ is married to ~ the person P2.');
|
||||
expect(node.concepts.person.is_married_to.name).to.be('person');
|
||||
});
|
||||
|
||||
it('conceptualise a ~ farmer ~ F that is a person and is a land owner.', function() {
|
||||
node.addCE('conceptualise a ~ person ~ p');
|
||||
node.addCE('conceptualise a ~ land owner ~ L');
|
||||
node.addCE('conceptualise a ~ farmer ~ F that is a person and is a land owner.');
|
||||
expect(node.concepts.farmer.parents.length).to.be(2);
|
||||
expect(node.concepts.farmer.parents[0].name).to.be('person');
|
||||
expect(node.concepts.farmer.parents[1].name).to.be('land owner');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|