Compare commits

..

5 Commits

14 changed files with 325 additions and 302 deletions

View File

@ -4,7 +4,7 @@ A pure JavaScript implementation of the ITA project's CEStore - called CENode. C
Please visit the project's [home page](http://cenode.io) for more information and for documentation.
**We recommend beginners check out the [Getting Started Guide](https://github.com/willwebberley/CENode/wiki/Getting-Started-Guide) before continuing.**
**We recommend beginners check out the [Getting Started Guide](https://github.com/flyingsparx/CENode/wiki/Getting-Started-Guide) before continuing.**
## Getting started
@ -33,13 +33,13 @@ const CEModels = require('cenode/models'); // if requred
const node = new CENode(CEModels.core);
```
See the [Wiki](https://github.com/willwebberley/CENode/wiki) for further guides and the API reference.
See the [Wiki](https://github.com/flyingsparx/CENode/wiki) for further guides and the API reference.
## Testing
Clone the repository
```
git clone git@github.com:willwebberley/CENode.git
git clone git@github.com:flyingsparx/CENode.git
```
Install the necessary dev dependencies.
@ -54,7 +54,7 @@ npm test
## More Information
See the CENode [Wiki](https://github.com/willwebberley/CENode/wiki) for more information, guides, and the API reference.
See the CENode [Wiki](https://github.com/flyingsparx/CENode/wiki) for more information, guides, and the API reference.
## Licence

21
langs/en.js Normal file
View File

@ -0,0 +1,21 @@
module.exports = {
concept: {
create: '^conceptualise an? ~ ([a-zA-Z0-9 ]*) ~ ([A-Z0-9]+)(?: that)?',
edit: '^conceptualise the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) (?:has|is|~)',
parseValue: 'has the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) as ~ ([a-zA-Z0-9 ]*) ~',
parseParent: '^is an? ([a-zA-Z0-9 ]*)',
parseRel: '~ ([a-zA-Z0-9 ]*) ~ the ([a-zA-Z0-9 ]*) ([A-Z0-9]+)',
parseSyn: '~ is expressed by ~ ([a-zA-Z0-9 ]*)'
},
instance: {
create: '^there is an? ([a-zA-Z0-9 ]*) named ([a-zA-Z0-9_]+|\'[a-zA-Z0-9_ ]+\')(?: that)?',
edit: 'the ([a-zA-Z0-9_ ]+) ([a-zA-Z0-9_]+|\'[a-zA-Z0-9_ ]+\')',
parseRel: '(?!has)([a-zA-Z0-9 ]*) the ([a-zA-Z0-9 ]*) ([a-zA-Z0-9_\' ]*)',
parseRawVal: '^has ([a-zA-Z0-9]*|\'[^\'\]*(?:\\.[^\'\]*)*\') as ([a-zA-Z0-9 ]*)',
parseInstanceVal: 'has the ([a-zA-Z0-9 ]*) ([a-zA-Z0-9_]*|\'[a-zA-Z0-9_ ]*\') as ([a-zzA-Z0-9 ]*)',
parseInstanceSubConcept: '(?:is| )?an? ([a-zA-Z0-9 ]*)',
parseInstanceSynonym: 'is expressed by (\'[a-zA-Z0-9 ]*\'|[a-zA-Z0-9]*)'
},
and: 'and',
value: 'value'
};

3
langs/index.js Normal file
View File

@ -0,0 +1,3 @@
var en = require('./en.js');
module.exports = {en};

View File

@ -18,7 +18,6 @@ module.exports = [
"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 ~ gist 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 ~",

View File

@ -1,13 +1,13 @@
{
"name": "cenode",
"version": "3.0.12",
"version": "3.0.8",
"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/willwebberley/CENode"
"url": "https://github.com/flyingsparx/CENode"
},
"files": [
"src",

View File

@ -16,6 +16,7 @@
*/
'use strict';
const LanguageManager = require('./LanguageManager.js');
const CEAgent = require('./CEAgent.js');
const CEParser = require('./CEParser.js');
const QuestionParser = require('./QuestionParser.js');
@ -118,6 +119,7 @@ class CENode {
const descendants = concept.descendants.concat(concept);
const childrenIds = [];
for (const descendant of descendants) { childrenIds.push(descendant.id); }
for (const ancestor of concept.ancestors) { childrenIds.push(ancestor.id); }
for (const instance of this.instances) {
if (instance && childrenIds.indexOf(instance.concept.id) > -1) {
instanceList.push(instance);
@ -137,12 +139,12 @@ class CENode {
*/
addSentence(sentence, source) {
const ceResult = this.addCE(sentence, false, source);
if (!ceResult.error) {
if (ceResult.success) {
return ceResult;
}
const questionResult = this.askQuestion(sentence);
if (!questionResult.error) {
if (questionResult.success) {
return questionResult;
}
@ -170,7 +172,13 @@ class CENode {
* Returns: {success: bool, type: str, data: str}
*/
addCE(sentence, source) {
return this.ceParser.parse(sentence.trim().replace('{now}', new Date().getTime()).replace('{uid}', this.newCardId()), 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,
};
}
/*
@ -180,7 +188,12 @@ class CENode {
* Returns: {success: bool, type: str, data: str}
*/
askQuestion(sentence) {
return this.questionParser.parse(sentence);
const success = this.questionParser.parse(sentence);
return {
success: success[0],
type: success[0] ? 'gist' : undefined,
data: success[0] ? success[1] : undefined,
};
}
/*
@ -190,7 +203,11 @@ class CENode {
* Returns: {type: str, data: str}
*/
addNL(sentence) {
return this.nlParser.parse(sentence);
const success = this.nlParser.parse(sentence);
return {
type: success[0] ? 'confirm' : 'gist',
data: success[1],
};
}
/*
@ -230,6 +247,7 @@ class CENode {
* sentence sets to be processed.
*/
constructor(...models) {
this.languageManager = new LanguageManager(this);
this.ceParser = new CEParser(this);
this.questionParser = new QuestionParser(this);
this.nlParser = new NLParser(this);

View File

@ -18,6 +18,7 @@
const CEConcept = require('./CEConcept.js');
const CEInstance = require('./CEInstance.js');
const en = require('../langs/en.js');
const quotes = {
escape(string) {
@ -28,16 +29,17 @@ const quotes = {
},
};
const newConcept = new RegExp(en.concept.create, 'i');
const editConcept = new RegExp(en.concept.edit);
const newInstance = new RegExp(en.instance.create);
const editInstance = new RegExp(en.instance.edit);
const andRegex = new RegExp('\\b' + en.and + '\\b', 'gi');
const and = en.and;
const value = en.value;
class CEParser {
static error(message, concerns) {
return { error: true, response: { message, type: 'gist', concerns } };
}
static success(message, concerns) {
return { error: false, response: { message, type: 'gist', concerns } };
}
/*
* Submit CE to be processed by node.
* This may result in
@ -50,85 +52,85 @@ class CEParser {
* Returns: [bool, str] (bool = success, str = error or parsed string)
*/
parse(input, source) {
try {
// Whitespace -> single space:
const t = input.replace(/\s+/g, ' ').replace(/\.+$/, '').trim();
if (t.match(/^conceptualise an?/i)) {
return this.newConcept(t, source);
} else if (t.match(/^conceptualise the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) (?:has|is|~)/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 CEParser.error('Input not a valid CE sentence.');
} catch (err) {
return CEParser.error(`There was a problem parsing the CE. ${err}.`);
const t = input.replace(/\s+/g, ' ').replace(/\.+$/, '').trim(); // Whitespace -> single space
if (newConcept.test(t)){
return this.newConcept(t, source);
} else if (editConcept.test(t)) {
return this.modifyConcept(t, source);
} else if (newInstance.test(t)) {
return this.newInstance(t, source);
} else if (editInstance.test(t)) {
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 match = newConcept.exec(t);
const conceptName = match[1];
const storedConcept = this.node.getConceptByName(conceptName);
let concept = null;
if (storedConcept) {
return CEParser.error('This concept already exists');
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);
const remainder = t.replace(newConcept, '');
const facts = remainder.replace(andRegex, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
if (facts){
for (const fact of facts) {
this.processConceptFact(concept, fact, source);
}
}
return CEParser.success(t, concept);
return [true, t, concept];
}
modifyConcept(t, source) {
const conceptInfo = t.match(/^conceptualise the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) (?:has|is|~)/);
const conceptInfo = editConcept.exec(t);
if (!conceptInfo) {
return CEParser.error('Unable to parse sentence');
return [false, 'Unable to parse sentence'];
}
const conceptName = conceptInfo[1];
const conceptVar = conceptInfo[2];
const concept = this.node.getConceptByName(conceptName);
if (!concept) {
return CEParser.error(`Concept ${conceptInfo[1]} not known.`);
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);
const facts = remainder.replace(andRegex, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
for (const fact of facts) {
this.processConceptFact(concept, fact, source);
}
return CEParser.success(t, concept);
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 parseVal = new RegExp(en.concept.parseValue);
const parsePar = new RegExp(en.concept.parseParent);
const parseRel = new RegExp(en.concept.parseRel);
const parseSyn = new RegExp(en.concept.parseSyn);
const input = fact.trim().replace(/\+/g, and);
if (parseVal.test(input)){
const match = parseVal.exec(input);
const valConceptName = match[1];
const label = match[3];
const valConcept = valConceptName === 'value' ? 0 : this.node.getConceptByName(valConceptName);
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);
if (parsePar.test(input)){
const match = parsePar.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);
if (parseRel.test(input)){
const match = parseRel.exec(input);
const label = match[1];
const relConceptName = match[2];
const relConcept = this.node.getConceptByName(relConceptName);
@ -136,71 +138,77 @@ class CEParser {
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);
if (parseSyn.test(input)){
const match = parseSyn.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 CEParser.error('Unable to determine name of instance.'); }
}
const names = newInstance.exec(t)
const conceptName = names[1];
const instanceName = names[2].replace(/\\/g, '');
const instanceName = names[2].replace(/\\/g, '').replace(/'/g, '');
const concept = this.node.getConceptByName(conceptName);
const currentInstance = this.node.getInstanceByName(instanceName, concept);
if (!concept) {
return CEParser.error(`Instance type unknown: ${conceptName}`);
return [false, `Instance type unknown: ${conceptName}`];
}
if (currentInstance && currentInstance.type.id === concept.id) {
return CEParser.error('There is already an instance of this type with this name.', currentInstance);
return [false, 'There is already an instance of this type with this name.', currentInstance];
}
const instance = new CEInstance(this.node, concept, instanceName, source);
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);
const remainder = t.replace(newInstance, '');
const facts = remainder.replace(andRegex, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
if (facts){
for (const fact of facts) {
this.processInstanceFact(instance, fact, source);
}
}
return CEParser.success(t, instance);
return [true, t, instance];
}
modifyInstance(t, source) {
let concept;
let instance;
let instanceName;
if (t.match(/^the ([a-zA-Z0-9 ]*)/i)) {
const names = t.match(/^the ([a-zA-Z0-9 ]*)/i);
const names = editInstance.exec(t);
concept = this.node.getConceptByName(names[1]);
if (concept){
instance = this.node.getInstanceByName(names[2].replace(/\\/g, '').replace(/'/g, ''));
}
else {
const nameTokens = names[1].split(' ');
for (const conceptCheck of this.node.concepts) {
if (names[1].toLowerCase().indexOf(conceptCheck.name.toLowerCase()) === 0) {
concept = conceptCheck;
instanceName = nameTokens[concept.name.split(' ').length];
instance = this.node.getInstanceByName(instanceName, concept);
let currentName = '';
for (const index in nameTokens){
currentName += ' ' + nameTokens[index];
concept = this.node.getConceptByName(currentName.trim());
if (concept){
break;
}
}
}
if (!instance && 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]);
instanceName = names[2].replace(/\\/g, '');
instance = this.node.getInstanceByName(instanceName, concept);
if (concept){
const possibleInstances = this.node.getInstances(concept.name, true);
let lowestIndex = null;
for (const potential of possibleInstances){
const check = new RegExp('\\b(' + potential.name + (potential.synonyms.length ? '|' + potential.synonyms.join('|') : '') + ')\\b', 'i');
const match = check.exec(t);
if (match && (lowestIndex === null || match.index < lowestIndex)){
lowestIndex = match.index;
instance = potential;
}
}
}
}
if (!concept || !instance) {
return CEParser.error(`Unknown concept/instance combination in: ${t}`);
return [false, `Unknown concept/instance combination in: ${t}`];
}
instance.sentences.push(t);
const tokens = t.split(' ');
tokens.splice(0, 1 + concept.name.split(' ').length + instanceName.split(' ').length);
tokens.splice(0, 1 + concept.name.split(' ').length + instance.name.split(' ').length);
const remainder = tokens.join(' ');
const facts = remainder.replace(/\band\b/g, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
if (facts) {
@ -208,14 +216,19 @@ class CEParser {
this.processInstanceFact(instance, fact, source);
}
}
return CEParser.success(t, instance);
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 parseRel = new RegExp(en.instance.parseRel);
const parseRawVal = new RegExp(en.instance.parseRawVal);
const parseInstanceVal = new RegExp(en.instance.parseInstanceVal);
const parseInstanceSubConcept = new RegExp(en.instance.parseInstanceSubConcept);
const parseInstanceSynonym = new RegExp(en.instance.parseInstanceSynonym);
if (parseRel.test(input)){
const match = parseRel.exec(input);
const label = match[1];
const relConceptName = match[2];
const relInstanceName = match[3].replace(/'/g, '');
@ -229,16 +242,14 @@ class CEParser {
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);
if (parseRawVal.test(input)){
const match = parseRawVal.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);
if (parseInstanceVal.test(input)){
const match = parseInstanceVal.exec(input);
const valConceptName = match[1];
const valInstanceName = match[2].replace(/'/g, '');
const label = match[3];
@ -251,13 +262,12 @@ class CEParser {
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);
if (parseInstanceSubConcept.test(input)){
const match = parseInstanceSubConcept.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]*)/);
if (parseInstanceSynonym.test(input)){
const match = parseInstanceSynonym.exec(input);
const synonym = match && match[1] && match[1].replace(/'/g, '').trim();
instance.addSynonym(synonym);
}

View File

@ -116,22 +116,14 @@ class CEServer {
},
'/instance': (request, response) => {
const idRegex = decodeURIComponent(request.url).match(/id=(.*)/);
const nameRegex = decodeURIComponent(request.url).match(/name=(.*)/);
const idQuery = idRegex ? idRegex[1] : null;
const nameQuery = nameRegex ? nameRegex[1] : null;
let instance;
if (idQuery) {
instance = this.node.getInstanceById(idQuery);
} else if (nameQuery) {
instance = this.node.getInstanceByName(nameQuery);
}
const id = idRegex ? idRegex[1] : null;
const instance = this.node.getInstanceById(id);
if (instance) {
const body = {
name: instance.name,
conceptName: instance.concept.name,
conceptId: instance.concept.id,
ce: instance.ce,
gist: instance.gist,
synonyms: instance.synonyms,
subConcepts: [],
values: [],
@ -154,7 +146,7 @@ class CEServer {
return response.end(JSON.stringify(body));
}
response.writeHead(404);
return response.end('Unable to find the instance.');
return response.end('Concept not found');
},
'/info': (request, response) => {
const body = { recentInstances: [], recentConcepts: [], instanceCount: this.node.instances.length, conceptCount: this.node.concepts.length };
@ -254,12 +246,7 @@ class CEServer {
if (request.method in this.handlers) {
const path = request.url.indexOf('?') > 1 ? request.url.slice(0, request.url.indexOf('?')) : request.url;
if (path in this.handlers[request.method]) {
try {
this.handlers[request.method][path](request, response);
} catch (err) {
response.writeHead(500);
response.end(`500: ${err}.`);
}
this.handlers[request.method][path](request, response);
} else {
response.writeHead(404);
response.end(`404: Resource not found for method ${request.method}.`);

View File

@ -38,9 +38,9 @@ class CardHandler {
// Prepare the response 'tell card' and add this back to the node
let urls;
let c;
if (data.response.message) {
urls = data.response.message.match(/(https?:\/\/[a-zA-Z0-9./\-+_&=?!%]*)/gi);
c = `there is a ${data.response.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.response.message.replace(/'/g, "\\'")}' as content`;
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`;
}
@ -63,12 +63,12 @@ class CardHandler {
// Add the CE sentence to the node
const data = this.node.addCE(card.content, card.is_from && card.is_from.name);
if (data.error && card.is_from) {
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.error) {
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) {
@ -86,12 +86,12 @@ class CardHandler {
const ack = policy.acknowledgement;
if (policy.target.name.toLowerCase() === card.is_from.name.toLowerCase()) {
let c;
if (ack === 'basic') { c = 'OK.'; } else if (data.response.type === 'tell') {
c = `OK. I added this to my knowledge base: ${data.response.message}`;
} else if (data.response.type === 'ask' || data.response.type === 'confirm' || data.response.type === 'gist') {
c = data.response.message;
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.response.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 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}'.`);
}
}
}
@ -102,28 +102,17 @@ class CardHandler {
'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.error) {
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.error) {
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.response.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.response.message.replace(/'/g, "\\'")}' as content and is in reply to the card '${card.name}'.`);
},
'gist card': (card) => {
// Add sentence to any active gist policy queues
for (const policy of this.node.getInstances('gist policy')) {
if (policy.enabled === 'true' && policy.target && policy.target.name) {
const targetName = policy.target.name;
if (!(targetName in this.agent.policyHandler.unsentGistCards)) { this.agent.policyHandler.unsentGistCards[targetName] = []; }
this.agent.policyHandler.unsentGistCards[targetName].push(card);
}
}
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}'.`);
},
};
}

89
src/LanguageManager.js Normal file
View File

@ -0,0 +1,89 @@
/*
* 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 langs = require('../langs');
const placeholders = {
conceptName: '([a-zA-Z0-9 ]*)',
conceptVar: '([A-Z0-9]+)',
instanceName: '([a-zA-Z0-9_]+|\'[a-zA-Z0-9_ ]+\')',
relationshipLabel: '([a-zA-Z0-9 ]*)'
};
class LanguageManager {
static getEntry(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1');
s = s.replace(/^\./, '');
const a = s.split('.');
for (let i = 0, n = a.length; i < n; ++i) {
const k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}
getExpression(key) {
let pattern = LanguageManager.getEntry(this.lang, key);
const extractions = {};
if (pattern){
console.log(pattern)
for (const placeholder in placeholders){
if (pattern.indexOf(placeholder) > -1){
const re = new RegExp('<' + placeholder + '>');
pattern = pattern.replace(re, placeholders[placeholder]);
}
}
}
console.log(pattern)
return pattern;
}
is(key, string){
const re = new RegExp(this.getExpression(key), 'i');
return re.test(string);
}
parse(key, string){
const re = new RegExp(this.getExpression(key), 'i');
return re.exec(string);
}
extract(key, string){
}
addLanguage(key, language){
langs[key] = language;
}
setLanguage(key) {
if (key in langs){
this.lang = langs[key];
}
}
constructor(node) {
this.node = node;
this.lang = langs['en'];
}
}
module.exports = LanguageManager;

View File

@ -18,14 +18,6 @@
class NLParser {
static error(message) {
return { error: true, response: { message, type: 'gist' } };
}
static success(message) {
return { error: false, response: { message, type: 'confirm' } };
}
/*
* Submit natural language to be processed by node.
* This results in
@ -130,7 +122,7 @@ class NLParser {
}
}
if (facts.length > 0) {
return NLParser.success(ce + facts.join(' and '));
return [true, ce + facts.join(' and ')];
}
}
@ -148,12 +140,12 @@ class NLParser {
}
}
if (newInstanceName && newInstanceName.length) {
return NLParser.success(`there is a ${this.node.concepts[i].name} named '${newInstanceName.trim()}'`);
return [true, `there is a ${this.node.concepts[i].name} named '${newInstanceName.trim()}'`];
}
return NLParser.success(`there is a ${this.node.concepts[i].name} named '${this.node.concepts[i].name} ${this.node.instances.length}${1}'`);
return [true, `there is a ${this.node.concepts[i].name} named '${this.node.concepts[i].name} ${this.node.instances.length}${1}'`];
}
}
return NLParser.error(`Un-parseable input: ${t}`);
return [false, `Un-parseable input: ${t}`];
}
/*

View File

@ -86,7 +86,6 @@ class PolicyHandler {
this.node = agent.node;
this.unsentTellCards = {};
this.unsentAskCards = {};
this.unsentGistCards = {};
this.lastSuccessfulRequest = 0;
this.handlers = {
@ -160,36 +159,6 @@ class PolicyHandler {
}
},
'gist policy': (policy) => {
// For each gist 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) {
if (!(policy.target.name in this.unsentGistCards)) {
this.unsentGistCards[policy.target.name] = [];
}
let data = '';
for (const card of this.unsentGistCards[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.unsentGistCards[policy.target.name] = [];
});
}
}
},
'listen policy': (policy) => {
// Make request to target to get cards addressed to THIS agent
if (policy.target && policy.target.address) {

View File

@ -18,14 +18,6 @@
class QuestionParser {
static error(message) {
return { error: true, response: { message, type: 'gist' } };
}
static success(message) {
return { error: false, response: { message, type: 'gist' } };
}
/*
* Submit a who/what/where question to be processed by node.
* This may result in
@ -36,25 +28,21 @@ class QuestionParser {
* Returns: [bool, str] (bool = success, str = error or response)
*/
parse(t) {
try {
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 QuestionParser.error('Input is not a valid question');
} catch (err) {
return QuestionParser.error(`There was a problem with the question. ${err}.`);
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) {
@ -62,7 +50,8 @@ class QuestionParser {
const instance = this.node.getInstanceByName(thing);
let message;
if (!instance) {
return QuestionParser.success(`I don't know what ${thing} is.`);
message = `I don't know what ${thing} is.`;
return [true, message];
}
const locatableInstances = this.node.getInstances('location', true);
const locatableIds = [];
@ -91,7 +80,8 @@ class QuestionParser {
}
}
if (!placeFound) {
return QuestionParser.success(`I don't know where ${instance.name} is.`);
message = `I don't know where ${instance.name} is.`;
return [true, message];
}
message = instance.name;
for (const place in places) {
@ -103,7 +93,7 @@ class QuestionParser {
message += ' and';
}
}
return QuestionParser.success(`${message.substring(0, message.length - 4)}.`);
return [true, `${message.substring(0, message.length - 4)}.`];
}
whatIsIn(t) {
@ -116,7 +106,7 @@ class QuestionParser {
}
}
if (!instance) {
return QuestionParser.success(`${thing} is not an instance of type location.`);
return [true, `${thing} is not an instance of type location.`];
}
const things = {};
let thingFound = false;
@ -143,7 +133,7 @@ class QuestionParser {
}
}
if (!thingFound) {
return QuestionParser.success(`I don't know what is located in/on/at the ${instance.type.name} ${instance.name}.`);
return [true, `I don't know what is located in/on/at the ${instance.type.name} ${instance.name}.`];
}
let message = '';
@ -154,7 +144,7 @@ class QuestionParser {
}
message += ' and';
}
return QuestionParser.success(`${message.substring(0, message.length - 4)}.`);
return [true, `${message.substring(0, message.length - 4)}.`];
}
whatIs(input) {
@ -166,7 +156,7 @@ class QuestionParser {
if (name) {
instance = this.node.getInstanceByName(name[2]);
if (instance) {
return QuestionParser.success(instance.gist);
return [true, instance.gist];
}
}
@ -197,46 +187,44 @@ class QuestionParser {
}
}
if (possibilities.length > 0) {
return QuestionParser.success(`'${name}' ${possibilities.join(' and ')}.`);
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;
if (searchReturn) {
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]}'`;
}
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 += `, '${searchReturn[key][i]}'`;
}
if (i === searchReturn[key].length - 1) {
fuzzyGist += '. ';
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;
} else if (!fuzzyFound) {
fuzzyGist += `the ${key} '${searchReturn[key][0]}'. `;
} else {
fuzzyGist += `The ${key} '${searchReturn[key][0]}'. `;
}
fuzzyFound = true;
}
if (fuzzyFound) {
return QuestionParser.success(fuzzyGist);
return [true, fuzzyGist];
}
return QuestionParser.success('I don\'t know who or what that is.');
return [true, 'I don\'t know who or what that is.'];
}
return QuestionParser.success(concept.gist);
return [true, concept.gist];
}
return QuestionParser.success(instance.gist);
return [true, instance.gist];
}
whatDoes(t) {
@ -268,21 +256,20 @@ class QuestionParser {
property = instance.property(fixedPropertyName);
}
if (property) {
return QuestionParser.success(`${instance.name} ${fixedPropertyName} the ${property.type.name} ${property.name}.`);
return [true, `${instance.name} ${fixedPropertyName} the ${property.type.name} ${property.name}.`];
}
return QuestionParser.success(`Sorry - I don't know that property about the ${instance.type.name} ${instance.name}.`);
return [true, `Sorry - I don't know that property about the ${instance.type.name} ${instance.name}.`];
}
} catch (err) {
return QuestionParser.success('Sorry - I can\'t work out what you\'re asking.');
return [false, 'Sorry - I can\'t work out what you\'re asking.'];
}
return QuestionParser.success('Sorry - I can\'t work out what you\'re asking about.');
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(' ');
const uniqueResponses = new Set([]);
let instance;
for (let i = 0; i < tokens.length; i += 1) {
const testString = tokens.slice(tokens.length - (i + 1), tokens.length).join(' ').trim();
@ -296,7 +283,6 @@ class QuestionParser {
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) {
@ -318,16 +304,12 @@ class QuestionParser {
property = subject.property(fixedPropertyName);
}
if (property && property.name === instance.name) {
uniqueResponses.add(`${subject.name} ${fixedPropertyName} the ${property.type.name} ${property.name}.`);
}
const responsesArray = Array.from(uniqueResponses);
if (responsesArray.length > 0 && i === this.node.instances.length - 1) {
return QuestionParser.success(responsesArray.join(' '));
return [true, `${subject.name} ${fixedPropertyName} the ${property.type.name} ${property.name}.`];
}
}
return QuestionParser.success(`Sorry - I don't know that property about the ${instance.type.name} ${instance.name}.`);
return [true, `Sorry - I don't know that property about the ${instance.type.name} ${instance.name}.`];
}
return QuestionParser.success('Sorry - I don\'t know the instance you\'re referring to.');
return null;
}
listInstances(t) {
@ -346,16 +328,17 @@ class QuestionParser {
s = 'All instances:';
}
if (ins.length === 0) {
return QuestionParser.success('I could not find any instances matching your query.');
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 QuestionParser.success(`${s} ${names.join(', ')}`);
return [true, `${s} ${names.join(', ')}`];
}
/*
*
* Search the knowledge base for an instance name similar to the one asked about.
*/
fuzzySearch(sentence) {
@ -365,10 +348,12 @@ class QuestionParser {
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())) {
@ -379,6 +364,7 @@ class QuestionParser {
instancesFiltered = instancesFiltered.concat(instancesFilteredTemp);
}
} else {
// single search term
instancesFiltered = instances.filter((input) => {
if (input.name.toUpperCase().includes(searchFor.toUpperCase())) {
return input;

View File

@ -1,40 +0,0 @@
const CENode = require('../src/CENode.js');
const CEModels = require('../models/index.js');
const expect = require('expect.js');
const myName = 'User'
const 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 ~ star ~ S that is a celestial body.",
"there is a star named sun.",
"there is a planet named Venus that orbits the star 'sun' and has 'media/Venus.jpg' as image.",
"there is a planet named Mercury that orbits the star 'sun' and has 'media/Mercury.jpg' as image."
]
let node;
describe('CEQuestionParser', function() {
describe('What relation questions', function () {
this.timeout(2050);
before(function() {
node = new CENode(CEModels.core, PLANETS_MODEL);
node.attachAgent();
node.agent.setName('agent1');
});
it('returns the correct number of responses', (done) => {
const message = 'what orbits the sun?';
const askCard = "there is a nl card named '{uid}' that is to the agent 'agent1' and is from the individual '" + myName + "' and has the timestamp '{now}' as timestamp and has '" + message.replace(/'/g, "\\'")+"' as content.";
node.addSentence(askCard);
setTimeout(function() {
const cards = node.concepts.card.allInstances;
const card = cards[cards.length - 1];
expect(card.content).to.equal('Venus orbits the star sun. Mercury orbits the star sun.');
done();
}, 2000);
});
});
});