Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
755ff6827b | ||
|
204440c5f2 |
@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
6
.gitignore
vendored
Executable file → Normal file
@ -1,7 +1,3 @@
|
||||
testing.js
|
||||
node_modules/
|
||||
lib/
|
||||
dist/
|
||||
*.sw*
|
||||
.DS_Store
|
||||
*.bbl
|
||||
@ -17,5 +13,3 @@ dist/
|
||||
*.out
|
||||
*.ps
|
||||
*.toc
|
||||
*.gz
|
||||
index.html
|
||||
|
@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
entry: './lib/CENode.js',
|
||||
entry: {
|
||||
CENode: './lib/CENode.js',
|
||||
CEModels: './models/index.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'dist/[name].js',
|
||||
library: '[name]',
|
||||
libraryTarget: 'var'
|
||||
}
|
||||
}
|
0
LICENCE → LICENSE
Executable file → Normal file
63
README.md
Executable file → Normal file
@ -1,62 +1,11 @@
|
||||
# CENode
|
||||
# cenode.js
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Please visit the project's [home page](http://cenode.io) for more information and for documentation.
|
||||
## Documentation
|
||||
|
||||
**We recommend beginners check out the [Getting Started Guide](https://github.com/willwebberley/CENode/wiki/Getting-Started-Guide) before continuing.**
|
||||
Please see the file `docs/documentation.pdf` for an overview and for technical information on the programmatic and RESTful APIs.
|
||||
|
||||
## Getting started
|
||||
## License
|
||||
|
||||
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.
|
||||
|
||||
Then add CENode to your project using NPM:
|
||||
```
|
||||
npm install cenode
|
||||
```
|
||||
|
||||
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, if using in a node app:
|
||||
```javascript
|
||||
const CENode = require('cenode');
|
||||
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.
|
||||
|
||||
## Testing
|
||||
|
||||
Clone the repository
|
||||
```
|
||||
git clone git@github.com:willwebberley/CENode.git
|
||||
```
|
||||
|
||||
Install the necessary dev dependencies.
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Run tests.
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
## More Information
|
||||
|
||||
See the CENode [Wiki](https://github.com/willwebberley/CENode/wiki) for more information, guides, and the API reference.
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
CENode is released under the Apache Licence v2. See `LICENCE` for further information.
|
||||
`cenode.js` is released under the Apache License v2. See `LICENSE` for further information.
|
||||
|
31
docs/citations.bib
Normal file
@ -0,0 +1,31 @@
|
||||
@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}
|
||||
}
|
BIN
docs/documentation.pdf
Normal file
542
docs/documentation.tex
Normal file
@ -0,0 +1,542 @@
|
||||
\documentclass{scrartcl}
|
||||
\usepackage{framed}
|
||||
\usepackage{hyperref}
|
||||
|
||||
\title{\texttt{cenode.js}}
|
||||
\subtitle{Revision 1.1.1}
|
||||
\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 \texttt{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.
|
||||
|
||||
\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 \texttt{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):\\
|
||||
\texttt{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:\\
|
||||
\texttt{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 \texttt{teaches} relationship and have \texttt{subject} and \texttt{age} values.\\
|
||||
|
||||
New instances of an existing concept can be declared with normal CE:\\
|
||||
\texttt{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:\\
|
||||
\texttt{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 \texttt{subject}, then a new instance of type \texttt{subject} named `Computing' will be created. The same applies for the \texttt{class} `B2'. Since \texttt{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:\\
|
||||
\texttt{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:\\
|
||||
\texttt{Mrs Smith teaches the class B2 and has the subject computing as subject and has '45' as age.}
|
||||
|
||||
\subsubsection{Question-asking}
|
||||
|
||||
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 \texttt{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:\\
|
||||
\texttt{what is mrs smith?}\\
|
||||
\texttt{who is mrs smith?}\\
|
||||
|
||||
Both of these questions would result in a gist output looking something like the following:\\
|
||||
\texttt{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\\
|
||||
\texttt{what is a teacher?}\\
|
||||
would result output similar to:\\
|
||||
\texttt{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\\
|
||||
\texttt{what is teaches?}\\
|
||||
would give:\\
|
||||
\texttt{'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 \texttt{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 \texttt{location} as an ancestor, then a meaningful response can be obtained.\\
|
||||
|
||||
For example, let's assume that the \texttt{person} concept (that \texttt{teacher} inherits from) supports a relationship called `lives in' that targets an instance of type \texttt{house}, which is a child of \texttt{location}:\\
|
||||
\texttt{the teacher Mrs Smith lives in the house 'Number 23'}.\\
|
||||
|
||||
We can now ask a `where' question:\\
|
||||
\texttt{Where is Mrs Smith?}\\
|
||||
and receive a response:\\
|
||||
\texttt{Mrs Smith lives in the house 'Number 23'}\\
|
||||
|
||||
In general, CENode ignores stop words and punctuation, so the following are all valid questions:\\
|
||||
\texttt{what is an apple?}\\
|
||||
\texttt{where is the banana?}\\
|
||||
\texttt{Who is Mrs Smith}
|
||||
|
||||
|
||||
\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.\\
|
||||
|
||||
\texttt{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 \texttt{location} concept, as well as other concepts that are useful to subclass when further populating the model. These models are included in \texttt{cenode.js}'s \texttt{MODELS} object, so that the CORE model can be accessed by \texttt{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 \texttt{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):\\
|
||||
\texttt{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 \texttt{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 \texttt{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 \texttt{tell card}:\\
|
||||
\texttt{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 \texttt{tell card} should be used to tell a particular agent some information, and an \texttt{ask card} should be used to query for some information. Using what we've covered so far, all of the \texttt{conceptualise} and instance-manipulation sentences would go into a \texttt{tell card} and the questions discussed in Section 1.2.2 would be wrapped in an \texttt{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.
|
||||
|
||||
|
||||
\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 \texttt{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 \texttt{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 \texttt{cenode.js} library can be easily imported:\\
|
||||
\texttt{<script src="cenode.js"></script>}\\
|
||||
|
||||
Once imported, a new CENode instance can be instantiated in a later \texttt{<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 \texttt{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 \texttt{\{uid\}} and \texttt{\{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:\\
|
||||
\texttt{\# pacman -S nodejs}\\
|
||||
with Ubuntu:\\
|
||||
\texttt{\# apt-get install nodejs}\\
|
||||
and with OS X (with Homebrew installed):\\
|
||||
\texttt{\$ brew install node}\\
|
||||
|
||||
Please note that the library is also mostly compatible with other JavaScript runtimes, such as \texttt{io.js}.\\
|
||||
|
||||
Once 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}
|
||||
\texttt{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 \texttt{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_rest}.
|
||||
|
||||
\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 \texttt{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 \texttt{XMLHttpRequest} object, and if running as a Node.js app it will use Node.js's \texttt{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:\\
|
||||
\texttt{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{\texttt{tell policy}}
|
||||
A \texttt{tell policy} inherits from \texttt{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:\\
|
||||
\texttt{there is an agent named 'agent2' that has 'agent2.address.com' as address.}\\
|
||||
We can now create a tell policy targeting this agent:\\
|
||||
\texttt{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{\texttt{ask policy}}
|
||||
An \texttt{ask policy} works in almost exactly the same way as a tell policy (with our local agent named `agent1'):\\
|
||||
\texttt{there is an ask policy named 'p1' that has 'true' as enabled and has the agent 'agent2' as target.}\\
|
||||
|
||||
In this scenario, every \texttt{ask card} sent to `agent1' will also be sent to `agent2' using a HTTP POST request. As with targets of a \texttt{tell policy}, target agents of an \texttt{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{\texttt{listen policy}}
|
||||
\label{listen_policy}
|
||||
A \texttt{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'):\\
|
||||
\begin{verbatim}
|
||||
- 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
|
||||
\end{verbatim}
|
||||
|
||||
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{\texttt{forwardall policy}}
|
||||
A \texttt{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 \texttt{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 \texttt{listen policy} targeting this node.\\
|
||||
|
||||
As with the previous examples, imagine the local agent which is acting on the \texttt{forwardall policy} is named `agent1'.\\
|
||||
|
||||
The construction of a \texttt{forwardall policy} might look like this:\\
|
||||
\texttt{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 \texttt{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:\\
|
||||
\texttt{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:\\
|
||||
\texttt{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:\\
|
||||
\texttt{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{\texttt{feedback policy}}
|
||||
A \texttt{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 \texttt{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 \texttt{forwardall policy} it will \textit{not} invoke a network request. Instead, any feedback is included in a \texttt{tell card} addressed to the target, which is written to the agent's own node. Thus, if responses are required over the network, a \texttt{listen policy} must also be used.\\
|
||||
|
||||
Since no network activity is directly involved (unless there is a \texttt{listen policy} in place), this type of policy is mostly useful for JavaScript or web applications using the \texttt{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 \texttt{tell card}s:\\
|
||||
\texttt{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 \texttt{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 \texttt{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:\\
|
||||
\begin{verbatim}
|
||||
- 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
|
||||
\end{verbatim}
|
||||
|
||||
Agent2's setup is symmetrical:\\
|
||||
\begin{verbatim}
|
||||
- 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
|
||||
\end{verbatim}
|
||||
|
||||
|
||||
\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:\\
|
||||
\begin{verbatim}
|
||||
- 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
|
||||
\end{verbatim}
|
||||
|
||||
Secondly, the router node needs to simply forward every message it receives on to every agent it knows about:\\
|
||||
\begin{verbatim}
|
||||
- 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
|
||||
\end{verbatim}
|
||||
|
||||
|
||||
\section{CENode API}
|
||||
\label{api}
|
||||
As discussed throughout this document, \texttt{cenode.js} can be used programmatically in a JavaScript or web application as well as a web service for receiving RESTful requests. This section describes the methods available on both types of interface.
|
||||
|
||||
\subsection{Programmatic Interface}
|
||||
When used as a library as part of a JavaScript application or within a webpage (see Sections \ref{as_a_webapp} and \ref{as_an_app}), \texttt{CENode} instances expose a number of useful public methods. All the methods in this section are callable on instances of \texttt{CENode}.
|
||||
|
||||
\newcommand{\cenodemethod}[5]{
|
||||
\begin{flushleft}
|
||||
\begin{minipage}{1.2\textwidth}
|
||||
\vskip10pt
|
||||
\begin{large}
|
||||
\noindent\texttt{#1 \underline{#2} (#3)}
|
||||
\end{large}
|
||||
\vskip5pt
|
||||
\begin{tabular}{|p{0.9\textwidth}}
|
||||
\textit{#4}
|
||||
\vskip8pt
|
||||
\textbf{Example Usage}\\
|
||||
#5
|
||||
\end{tabular}
|
||||
\end{minipage}
|
||||
\end{flushleft}
|
||||
}
|
||||
|
||||
\cenodemethod{CENode}{CENode}{[model1[, model2[, model3 ...]]]}{Instantiates and returns a new \texttt{CENode} object with any number of models to initially develop the node's KB. Generally, it is recommended that the default CORE model at least be loaded. Instantiating a CENode starts the lifecycle of an agent within it, whose name is set to `Moira' by default.}{
|
||||
\texttt{var node = new CENode(MODELS.CORE);}\\
|
||||
The Node can instead be instantiated with custom models. These can be created as described in Section \ref{models} and then loaded in the same way:\\
|
||||
\texttt{var node = new CENode(custom\_model\_1, custom\_model\_2);}
|
||||
}
|
||||
|
||||
\cenodemethod{String}{guess\_next}{String input}{Returns a guess of the rest of the CE sentence in \texttt{input} for supporting auto-complete. Note that this feature is still under development}{
|
||||
\texttt{var guess = node.guess\_next("there is a p");}\\
|
||||
In this case, the node will look through its knowledge base to find concepts with name starting with `p' (for example, `person'). \texttt{guess} would then contain `there is a person named'.
|
||||
}
|
||||
|
||||
\cenodemethod{Instance[]}{get\_instances}{[String concept\_name[, Bool recurse]]}{Return a list of instance objects. If \texttt{concept\_name} is included, then only instances of this type will be included. If \texttt{recurse} is set to \texttt{true} then instances of the concept's children, grandchildren, etc., will also be included.}{
|
||||
\texttt{var tell\_cards = node.get\_instances("tell card");}\\
|
||||
\texttt{var all\_cards = node.get\_instances("card", true);}
|
||||
}
|
||||
|
||||
\cenodemethod{Concept[]}{get\_concepts}{}{Return a list of concept objects known by the node.}{
|
||||
\texttt{var concepts = node.get\_concepts();}\\
|
||||
}
|
||||
|
||||
\cenodemethod{String}{get\_instance\_type}{Instance instance}{Return a string representing the type (i.e. the name of the concept) of the given instance.}{
|
||||
\texttt{var concept\_name = node.get\_instance\_type(instance);}\\
|
||||
}
|
||||
|
||||
\cenodemethod{String}{get\_instance\_ce}{Instance instance}{Return a string representing the CE required to construct the instance.}{
|
||||
\texttt{var ce = node.get\_instance\_ce(instance);}\\
|
||||
`ce' will be of the form \texttt{"there is a teacher named 'Mrs Smith' that..."}.
|
||||
}
|
||||
|
||||
\cenodemethod{String}{get\_instance\_gist}{Instance instance}{Return a string representing a gist description of the instance.}{
|
||||
\texttt{var gist = node.get\_instance\_gist(instance);}\\
|
||||
`gist' will be of the form \texttt{"'Mrs Smith' is a teacher. Mrs Smith teaches the class B2 and..."}.
|
||||
}
|
||||
|
||||
\cenodemethod{String}{get\_concept\_ce}{Concept concept}{Return a string representing the CE required to construct the concept.}{
|
||||
\texttt{var ce = node.get\_concept\_ce(concept);}\\
|
||||
`ce' will be of the form \texttt{"conceptualise a $\sim$ teacher $\sim$ T that..."}.
|
||||
}
|
||||
|
||||
\cenodemethod{String}{get\_concept\_gist}{Concept concept}{Return a string representing a gist description of the concept.}{
|
||||
\texttt{var gist = node.get\_concept\_gist(concept);}\\
|
||||
`gist' will be of the form \texttt{"A teacher is a type of person and teaches a type of class and..."}.
|
||||
}
|
||||
|
||||
\cenodemethod{[bool, String]}{add\_ce}{String ce[, bool nowrite]}{Immediately updates the node's KB, as long as `ce' is valid CE. The returned \texttt{bool} indicates whether the CE-parsing was successful (\texttt{true} indicates valid CE) and the returned \texttt{String} will contain the input CE when it is valid, or an error message if it is not. `nowrite' is an optional boolean argument, which, if set to \texttt{true}, will not update the KB during parsing.\newline\texttt{add\_ce} can parse special character sequences to aid users in creating cards. The sequence \texttt{\{now\}} will be replaced by the current timestamp of the node's environment and \texttt{\{uid\}} will be replaced by an an appropriate identifier for the card.}{
|
||||
\texttt{var data = node.add\_ce("there is a teacher named 'Mrs Smith'");}
|
||||
}
|
||||
|
||||
\cenodemethod{[bool, String]}{ask\_question}{String question}{Immediately return a response to a KB query, as long as `question' is a valid question type. The returned \texttt{bool} indicates whether the question-parsing was successful (\texttt{true} indicates valid question) and the returned \texttt{String} will contain the response gist when it is valid, or an error message if it is not.}{
|
||||
\texttt{var data = node.ask\_question("who is Mrs Smith?");}
|
||||
}
|
||||
|
||||
\cenodemethod{[bool, String]}{add\_nl}{String nl}{Attempts to build a valid CE sentence based on the NL `nl' input. The returned \texttt{bool} indicates whether an attempt was made (\texttt{true} indicates returned CE) and the returned \texttt{String} will contain the attempt if it is valid, or an error message if it is not.}{
|
||||
\texttt{var data = node.add\_nl("there is a teacher called Mrs Smith");}
|
||||
}
|
||||
|
||||
\cenodemethod{[bool, String]}{add\_sentence}{String sentence[, bool nowrite]}{Adds a sentence to be processed by the node. In turn, this will first attempt to parse CE, then a question, and finally NL. The returned \texttt{bool} represents whether or not any of the above was successful and the returned \texttt{String}'s content varies. For more information, see the documentation for \texttt{add\_ce}, \texttt{ask\_question}, and \texttt{add\_nl} respectively. \newline\texttt{add\_sentence} can parse special character sequences to aid users in creating cards. The sequence \texttt{\{now\}} will be replaced by the current timestamp of the node's environment and \texttt{\{uid\}} will be replaced by an an appropriate identifier for the card.}{
|
||||
\texttt{node.add\_sentence("there is a teacher named 'Mrs Smith'");}\\
|
||||
\texttt{var answer = node.add\_sentence("Who is Mrs Smith?");}\\
|
||||
\vskip5pt
|
||||
The example below illustrates the use of special character sequences.
|
||||
\texttt{node.add\_sentence("there is a tell card named '\{uid\}' that has the timestamp '\{now\}' as timestamp ...");}
|
||||
}
|
||||
|
||||
\cenodemethod{String}{add\_sentences}{String[] sentences}{Adds an array of sentences to the model. Internally, this uses \texttt{add\_sentence}, so the above information applies. Responses made by the node are also returned in an array, where the ordered elements in the responses array are associated with the appropriate elements in the sentences array.}{
|
||||
\texttt{var sentences = [}\\
|
||||
\texttt{"conceptualise a $\sim$ teacher $\sim$ T that is a person",}\\
|
||||
\texttt{"there is a teacher named 'Mrs Smith'"}\\
|
||||
\texttt{]}\\
|
||||
|
||||
\texttt{var responses = node.add\_sentences(sentences);}\\
|
||||
}
|
||||
|
||||
\cenodemethod{void}{set\_agent\_name}{String name}{Sets a new name for the node's local agent. Updating this value will mean the agent will open different cards and will ignore any further cards sent to its previous name. Note that agent names are case-insensitive when qualifying card recipients.}{
|
||||
\texttt{node.set\_agent\_name("Agent 1");}
|
||||
}
|
||||
|
||||
\cenodemethod{String}{get\_agent\_name}{}{Retrieves the name of the local agent.}{
|
||||
\texttt{var agent\_name = node.get\_agent\_name();}
|
||||
}
|
||||
|
||||
\subsection{RESTful HTTP Interface}
|
||||
\label{api_rest}
|
||||
When run as a service (see Section \ref{as_a_service}), which runs on port 555 by default, users can visit \texttt{localhost:5555} to administer the node (use a different hostname if not running the node locally). The webpage served at this address allows administers to submit new sentences to the node, inspect instances and concepts, change the name of the local agent, load models, and reset the store.\\
|
||||
|
||||
In addition to these features, a node that is running as a service also exposes a RESTful interface for other nodes (or applications) to make HTTP requests to. There is no authentication mechanism implemented on \texttt{cenode.js} and the service will accept requests from any domain (i.e. CORS is enabled) so that nodes can be accessed from within web applications run within a browser. In fact, these RESTful endpoints are the same ones that are used by the agents themselves when acting on policies applied to them.
|
||||
|
||||
\cenodemethod{POST}{/sentences}{}{Submit a newline-separated set of CE sentences to the node. The sentences should be in the body of the request and no key is necessary. Nodes will return any content produced as a result of parsing the input sentences within the body of the response separated by new lines. If there is no response for an input sentence, then this will be represented by an empty string. This means that, for example, line 2 of the response body corresponds to the sentence on line 2 in the body of the request. \newline Remember that agents are asynchronous, so responses from `ask cards' will not be included in the response to \texttt{/sentences} - instead, use \texttt{GET /cards} to check for replies to `ask cards'.}{
|
||||
|
||||
REQUEST:
|
||||
\begin{framed}
|
||||
\texttt{POST /sentences\newline
|
||||
Content-Type: text/ce\newline\newline
|
||||
what is Mrs Smith?\newline
|
||||
there is a teacher named 'Mrs Smith'\newline
|
||||
what is Mrs Smith?\newline
|
||||
Mrs Smith teaches the class 'B2'
|
||||
}\end{framed}
|
||||
|
||||
RESPONSE:
|
||||
\begin{framed}
|
||||
\texttt{200 OK\newline
|
||||
Content-Type: text/ce\newline\newline
|
||||
I don't know what Mrs Smith is.\newline
|
||||
\newline
|
||||
Mrs Smith is a teacher.\newline
|
||||
}
|
||||
\end{framed}
|
||||
|
||||
}
|
||||
|
||||
\cenodemethod{GET}{/cards}{[?agent=NAME]}{Retrieve cards from the node. If `agent' is specified, then only cards addressed to `NAME' are returned. Cards are returned in pure CE in the body of the response, separated by newlines.}{
|
||||
|
||||
REQUEST:
|
||||
\begin{framed}
|
||||
\texttt{GET /cards?agent=agent1\newline}
|
||||
\end{framed}
|
||||
|
||||
RESPONSE:
|
||||
\begin{framed}
|
||||
\texttt{200 OK\newline
|
||||
Content-Type: text/ce\newline\newline
|
||||
there is a tell card named 'msg41' that is to the agent 'agent1'\newline
|
||||
there is an ask card named 'msg56' that is to the agent 'agent1'}
|
||||
\end{framed}
|
||||
(Note that the cards have been truncated for clarity).
|
||||
}
|
||||
|
||||
\bibliographystyle{plain}
|
||||
\bibliography{citations}
|
||||
|
||||
\end{document}
|
343
docs/index.html
Normal file
@ -0,0 +1,343 @@
|
||||
<!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 man named 'John Smith' that is a person of interest 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 try" href="test.html" 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 machine processable (with low complexity and no ambiguity) while also being human-readable and writable.</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.</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 Agent) 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>
|
BIN
docs/index_media/cardiff_logo.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
docs/index_media/chatboxes-white.png
Executable file
After Width: | Height: | Size: 9.4 KiB |
BIN
docs/index_media/chevron-right.png
Executable file
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/index_media/download-white.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
docs/index_media/gear.png
Executable file
After Width: | Height: | Size: 3.4 KiB |
BIN
docs/index_media/github-white-filled.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
docs/index_media/github-white.png
Executable file
After Width: | Height: | Size: 24 KiB |
BIN
docs/index_media/iphone-white.png
Executable file
After Width: | Height: | Size: 5.3 KiB |
BIN
docs/index_media/ita_logo.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/index_media/paper-white-filled.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
docs/index_media/paper-white.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/index_media/person.png
Executable file
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/index_media/play-white.png
Executable file
After Width: | Height: | Size: 7.7 KiB |
BIN
docs/index_media/quote-white.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
docs/index_media/quote.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
docs/index_media/text-white.png
Executable file
After Width: | Height: | Size: 6.8 KiB |
78
docs/test.html
Normal file
@ -0,0 +1,78 @@
|
||||
<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>
|
@ -1,26 +0,0 @@
|
||||
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 ~ 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 ~",
|
||||
"conceptualise a ~ feedback policy ~ P that is a policy and has the value V as ~ acknowledgement ~"
|
||||
];
|
@ -1,5 +0,0 @@
|
||||
var core = require('./core.js');
|
||||
var server = require('./server.js');
|
||||
var test = require('./test.js');
|
||||
|
||||
module.exports = {core, server, test};
|
@ -1,3 +0,0 @@
|
||||
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"
|
||||
];
|
@ -1,7 +0,0 @@
|
||||
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
@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "cenode",
|
||||
"version": "3.0.12",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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 (const instance of instances) {
|
||||
if (instance.name.toLowerCase() === name.toLowerCase()) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
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;
|
274
src/CEConcept.js
@ -1,274 +0,0 @@
|
||||
/*
|
||||
* 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) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
for (const concept of node.concepts) {
|
||||
if (concept.name.toLowerCase() === name.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
if (isNaN(name[0])) {
|
||||
const concept = this;
|
||||
Object.defineProperty(node.concepts, name.toLowerCase().replace(/ /g, '_'), {
|
||||
get() {
|
||||
return concept;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get instances() {
|
||||
const array = [];
|
||||
for (const instance of this.node.instances) {
|
||||
if (instance.concept.id === this.id) {
|
||||
array.push(instance);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
get allInstances() {
|
||||
const allConcepts = this.descendants.concat(this);
|
||||
const array = [];
|
||||
for (const instance of this.node.instances) {
|
||||
for (const concept of allConcepts) {
|
||||
if (instance.concept.id === concept.id) {
|
||||
array.push(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
get parents() {
|
||||
const array = [];
|
||||
for (const id of this.parentIds) {
|
||||
array.push(this.node.getConceptById(id));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
get ancestors() {
|
||||
const array = [];
|
||||
const stack = [];
|
||||
for (const parent of this.parents) {
|
||||
stack.push(parent);
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
array.push(current);
|
||||
for (const parent of current.parents) {
|
||||
stack.push(parent);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
get children() {
|
||||
const array = [];
|
||||
for (const concept of this.node.concepts) {
|
||||
for (const parent of concept.parents) {
|
||||
if (parent.id === this.id) {
|
||||
array.push(concept);
|
||||
}
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
get descendants() {
|
||||
const array = [];
|
||||
const stack = [];
|
||||
for (const child of this.children) {
|
||||
stack.push(child);
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
array.push(current);
|
||||
const currentChildren = current.children;
|
||||
if (currentChildren) {
|
||||
for (const child of currentChildren) {
|
||||
stack.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
get relationships() {
|
||||
const rels = [];
|
||||
for (const id of this.relationshipIds) {
|
||||
const relationship = {};
|
||||
relationship.label = id.label;
|
||||
relationship.concept = this.node.getConceptById(id.target);
|
||||
rels.push(relationship);
|
||||
}
|
||||
return rels;
|
||||
}
|
||||
|
||||
get values() {
|
||||
const vals = [];
|
||||
for (const val of this.valueIds) {
|
||||
const value = {};
|
||||
value.label = val.label;
|
||||
value.concept = val.type && this.node.getConceptById(val.type);
|
||||
vals.push(value);
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
|
||||
getCE(isModification) {
|
||||
let ce = '';
|
||||
if (isModification) {
|
||||
ce += `conceptualise the ${this.name} ${this.name.charAt(0).toUpperCase()}`;
|
||||
} else {
|
||||
ce += `conceptualise a ~ ${this.name} ~ ${this.name.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
if (!isModification && (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.parents.length; i += 1) {
|
||||
ce += ` is a ${this.parents[i].name}`;
|
||||
if (i < this.parents.length - 1) { ce += ' and'; }
|
||||
}
|
||||
}
|
||||
const 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} ~`);
|
||||
}
|
||||
}
|
||||
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) { ce += ' and'; }
|
||||
ce += ` ${facts.join(' and ')}`;
|
||||
}
|
||||
ce += '.';
|
||||
return ce;
|
||||
}
|
||||
|
||||
get creationCE() {
|
||||
return `conceptualise a ~ ${this.name} ~ ${this.name.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
|
||||
get ce() {
|
||||
return this.getCE();
|
||||
}
|
||||
|
||||
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);
|
||||
if (isNaN(label[0])) {
|
||||
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);
|
||||
if (isNaN(label[0])) {
|
||||
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 (const currentSynonym of this.synonyms) {
|
||||
if (currentSynonym.toLowerCase() === synonym.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.synonyms.push(synonym);
|
||||
}
|
||||
}
|
||||
module.exports = CEConcept;
|
@ -1,357 +0,0 @@
|
||||
/*
|
||||
* 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 || !name) {
|
||||
return;
|
||||
}
|
||||
for (const instance of node.instances) {
|
||||
if (instance.name.toLowerCase() === name.toLowerCase() && type.id === instance.concept.id) {
|
||||
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;
|
||||
|
||||
if (isNaN(name[0])) {
|
||||
const instance = this;
|
||||
const helperName = name.toLowerCase().replace(/ /g, '_').replace(/'/g, '');
|
||||
Object.defineProperty(node.instances, helperName, {
|
||||
get() {
|
||||
return instance;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(type, helperName, {
|
||||
get() {
|
||||
return instance;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get type() {
|
||||
for (const concept of this.node.concepts) {
|
||||
if (concept.id === this.concept.id) {
|
||||
return concept;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get relationships() {
|
||||
const rels = [];
|
||||
for (const id of this.relationshipIds) {
|
||||
const relationship = {};
|
||||
relationship.label = id.label;
|
||||
relationship.source = id.source;
|
||||
relationship.instance = this.node.getInstanceById(id.targetId);
|
||||
rels.push(relationship);
|
||||
}
|
||||
return rels;
|
||||
}
|
||||
|
||||
get values() {
|
||||
const vals = [];
|
||||
for (const id of this.valueIds) {
|
||||
const value = {};
|
||||
value.label = id.label;
|
||||
value.source = id.source;
|
||||
if (id.conceptId === 0) {
|
||||
value.instance = id.typeName;
|
||||
} else {
|
||||
value.instance = this.node.getInstanceById(id.conceptId);
|
||||
}
|
||||
vals.push(value);
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
|
||||
addSentence(sentence) {
|
||||
this.sentences.push(sentence);
|
||||
}
|
||||
|
||||
getPossibleProperties() {
|
||||
let ancestorInstances = this.concept.ancestors;
|
||||
ancestorInstances.push(this.concept);
|
||||
for (const subConcept of this.subConcepts) {
|
||||
ancestorInstances.push(subConcept);
|
||||
ancestorInstances = ancestorInstances.concat(subConcept.ancestors);
|
||||
}
|
||||
const properties = { values: [], relationships: [] };
|
||||
for (const ancestor of ancestorInstances) {
|
||||
for (const value of ancestor.values) {
|
||||
properties.values.push(value.label.toLowerCase());
|
||||
}
|
||||
for (const relationship of ancestor.relationships) {
|
||||
properties.relationships.push(relationship.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 && isNaN(valueNameField[0])) {
|
||||
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 (const id of this.valueIds) {
|
||||
if (id.label.toLowerCase().replace(/ /g, '_') === valueNameField) {
|
||||
instances.push(id.conceptId === 0 ? id.typeName : this.node.getInstanceById(id.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 && isNaN(relNameField[0])) {
|
||||
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 (const id of this.relationshipIds) {
|
||||
if (id.label.toLowerCase().replace(/ /g, '_') === relNameField) {
|
||||
instances.push(this.node.getInstanceById(id.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 (const checkSynonym of this.synonyms) {
|
||||
if (checkSynonym.toLowerCase() === synonym.toLowerCase()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this.synonyms.push(synonym);
|
||||
if (isNaN(synonym[0])) {
|
||||
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;
|
||||
}
|
||||
|
||||
getCE(isModification) {
|
||||
const concept = this.concept;
|
||||
if (!concept) { return ''; }
|
||||
|
||||
let ce = '';
|
||||
if (isModification) {
|
||||
ce += `the ${concept.name} '${this.name}'`;
|
||||
} else {
|
||||
ce += `there is a ${concept.name} named '${this.name}'`;
|
||||
}
|
||||
const facts = [];
|
||||
for (const subConcept of this.subConcepts) {
|
||||
facts.push(`is a ${subConcept.name}`);
|
||||
}
|
||||
for (const id of this.valueIds) {
|
||||
if (id.conceptId === 0) {
|
||||
facts.push(`has '${id.typeName.replace(/'/g, "\\'")}' as ${id.label}`);
|
||||
} else {
|
||||
const valueInstance = this.node.getInstanceById(id.conceptId);
|
||||
const valueConcept = valueInstance.type;
|
||||
facts.push(`has the ${valueConcept.name} '${valueInstance.name}' as ${id.label}`);
|
||||
}
|
||||
}
|
||||
for (const id of this.relationshipIds) {
|
||||
const relationshipInstance = this.node.getInstanceById(id.targetId);
|
||||
const relationshipConcept = relationshipInstance.type;
|
||||
facts.push(`${id.label} the ${relationshipConcept.name} '${relationshipInstance.name}'`);
|
||||
}
|
||||
if (facts.length > 0) { ce += `${!isModification && ' that'} ${facts.join(' and ')}`; }
|
||||
return `${ce}.`;
|
||||
}
|
||||
|
||||
get creationCE() {
|
||||
return `there is a ${this.concept && this.concept.name} named '${this.name}'`;
|
||||
}
|
||||
|
||||
get ce() {
|
||||
return this.getCE();
|
||||
}
|
||||
|
||||
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 (const subConcept of this.subConcepts) {
|
||||
gist += ` and ${vowels.indexOf(subConcept.name.toLowerCase()[0]) > -1 ? 'an' : 'a'} ${subConcept.name}`;
|
||||
}
|
||||
gist += '.';
|
||||
|
||||
const facts = {};
|
||||
let factFound = false;
|
||||
for (const id of this.valueIds) {
|
||||
factFound = true;
|
||||
let fact = '';
|
||||
if (id.conceptId === 0) {
|
||||
fact = `has '${id.typeName.replace(/'/g, "\\'")}' as ${id.label}`;
|
||||
} else {
|
||||
const valueInstance = this.node.getInstanceById(id.conceptId);
|
||||
const valueConcept = valueInstance.type;
|
||||
fact = `has the ${valueConcept.name} '${valueInstance.name}' as ${id.label}`;
|
||||
}
|
||||
if (!(fact in facts)) {
|
||||
facts[fact] = 0;
|
||||
}
|
||||
facts[fact] += 1;
|
||||
}
|
||||
for (const id of this.relationshipIds) {
|
||||
factFound = true;
|
||||
const relationshipInstance = this.node.getInstanceById(id.targetId);
|
||||
const relationshipConcept = relationshipInstance.type;
|
||||
const fact = `${id.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;
|
250
src/CENode.js
@ -1,250 +0,0 @@
|
||||
/*
|
||||
* 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 (const concept of this.concepts) {
|
||||
if (concept.name.toLowerCase() === name.toLowerCase()) {
|
||||
return concept;
|
||||
}
|
||||
for (const synonym of concept.synonyms) {
|
||||
if (synonym.toLowerCase() === name.toLowerCase()) {
|
||||
return concept;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getInstanceById(id) {
|
||||
return this.instanceDict[id];
|
||||
}
|
||||
|
||||
getInstanceByName(name, concept) {
|
||||
if (!name) { return null; }
|
||||
const possibleConcepts = concept ? [concept.id].concat(concept.ancestors.map(ancestor => ancestor.id)).concat(concept.descendants.map(descendant => descendant.id)) : [];
|
||||
for (const instance of this.instances) {
|
||||
if (instance && (concept ? possibleConcepts.indexOf(instance.concept.id) > -1 : true)) {
|
||||
if (instance.name.toLowerCase() === name.toLowerCase()) {
|
||||
return instance;
|
||||
}
|
||||
for (const synonym of instance.synonyms) {
|
||||
if (synonym.toLowerCase() === name.toLowerCase()) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
const instanceList = [];
|
||||
if (!conceptType) {
|
||||
for (const instance of this.instances) {
|
||||
instanceList.push(instance);
|
||||
}
|
||||
} else if (conceptType && !recurse) {
|
||||
const concept = this.getConceptByName(conceptType);
|
||||
if (concept) {
|
||||
for (const instance of this.instances) {
|
||||
if (instance && instance.concept.id === concept.id) {
|
||||
instanceList.push(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (conceptType && recurse === true) {
|
||||
const concept = this.getConceptByName(conceptType);
|
||||
if (concept) {
|
||||
const descendants = concept.descendants.concat(concept);
|
||||
const childrenIds = [];
|
||||
for (const descendant of descendants) { childrenIds.push(descendant.id); }
|
||||
for (const instance of this.instances) {
|
||||
if (instance && childrenIds.indexOf(instance.concept.id) > -1) {
|
||||
instanceList.push(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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.error) {
|
||||
return ceResult;
|
||||
}
|
||||
|
||||
const questionResult = this.askQuestion(sentence);
|
||||
if (!questionResult.error) {
|
||||
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 (const sentence of sentences) {
|
||||
responses.push(this.addSentence(sentence, 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) {
|
||||
return this.ceParser.parse(sentence.trim().replace('{now}', new Date().getTime()).replace('{uid}', this.newCardId()), source);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
return this.questionParser.parse(sentence);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
return this.nlParser.parse(sentence);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an array of CE sentences to the node.
|
||||
*
|
||||
* Returns: [[bool, str]...] (see signature for addCE)
|
||||
*/
|
||||
loadModel(sentences) {
|
||||
const responses = [];
|
||||
if (sentences && sentences.length) {
|
||||
for (const sentence of sentences) {
|
||||
responses.push(this.addCE(sentence));
|
||||
}
|
||||
}
|
||||
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 (const model of models) {
|
||||
this.loadModel(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = CENode;
|
270
src/CEParser.js
@ -1,270 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
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
|
||||
* - 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) {
|
||||
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}.`);
|
||||
}
|
||||
}
|
||||
|
||||
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 CEParser.error('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 CEParser.success(t, concept);
|
||||
}
|
||||
|
||||
modifyConcept(t, source) {
|
||||
const conceptInfo = t.match(/^conceptualise the ([a-zA-Z0-9 ]*) ([A-Z0-9]+) (?:has|is|~)/);
|
||||
if (!conceptInfo) {
|
||||
return CEParser.error('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.`);
|
||||
}
|
||||
|
||||
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 CEParser.success(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 CEParser.error('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, concept);
|
||||
if (!concept) {
|
||||
return CEParser.error(`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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
return CEParser.success(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 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);
|
||||
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 || !instance) {
|
||||
return CEParser.error(`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);
|
||||
const remainder = tokens.join(' ');
|
||||
const facts = remainder.replace(/\band\b/g, '+').match(/(?:'(?:\\.|[^'])*'|[^+])+/g);
|
||||
if (facts) {
|
||||
for (const fact of facts) {
|
||||
this.processInstanceFact(instance, fact, source);
|
||||
}
|
||||
}
|
||||
return CEParser.success(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);
|
||||
|
||||
if (relConcept) {
|
||||
let relInstance = this.node.getInstanceByName(relInstanceName, relConcept);
|
||||
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);
|
||||
if (valConcept) {
|
||||
let valInstance = this.node.getInstanceByName(valInstanceName, valConcept);
|
||||
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;
|
296
src/CEServer.js
@ -1,296 +0,0 @@
|
||||
/*
|
||||
* 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');
|
||||
|
||||
class CEServer {
|
||||
|
||||
constructor(name, port, models) {
|
||||
this.port = port;
|
||||
this.node = new CENode();
|
||||
if (models) {
|
||||
for (const model of models) {
|
||||
this.node.loadModel(CEModels[model]);
|
||||
}
|
||||
}
|
||||
this.node.attachAgent();
|
||||
this.node.agent.setName(name);
|
||||
this.handlers = {
|
||||
GET: {
|
||||
'/cards': (request, response) => {
|
||||
const agentRegex = decodeURIComponent(request.url).match(/agent=(.*)/);
|
||||
const agentStr = agentRegex ? agentRegex[1] : null;
|
||||
const agents = (agentStr && agentStr.toLowerCase().split(',')) || [];
|
||||
let s = '';
|
||||
for (const card of this.node.getInstances('card', true)) {
|
||||
for (const to of card.is_tos) {
|
||||
for (const agent of agents) {
|
||||
if (to.name.toLowerCase() === agent) {
|
||||
s += `${card.ce}\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce' });
|
||||
response.end(s);
|
||||
},
|
||||
'/concepts': (request, response) => {
|
||||
const concepts = [];
|
||||
for (const concept of this.node.concepts) {
|
||||
concepts.push({
|
||||
name: concept.name,
|
||||
id: concept.id,
|
||||
});
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify(concepts));
|
||||
},
|
||||
'/concept': (request, response) => {
|
||||
const idRegex = decodeURIComponent(request.url).match(/id=(.*)/);
|
||||
const id = idRegex ? idRegex[1] : null;
|
||||
const concept = this.node.getConceptById(id);
|
||||
if (concept) {
|
||||
const body = { name: concept.name, ce: concept.ce, parents: [], children: [], instances: [], values: [], relationships: [] };
|
||||
for (const parent of concept.parents) {
|
||||
body.parents.push({
|
||||
name: parent.name,
|
||||
id: parent.id,
|
||||
});
|
||||
}
|
||||
for (const child of concept.children) {
|
||||
body.children.push({
|
||||
name: child.name,
|
||||
id: child.id,
|
||||
});
|
||||
}
|
||||
for (const instance of concept.instances) {
|
||||
body.instances.push({
|
||||
name: instance.name,
|
||||
id: instance.id,
|
||||
});
|
||||
}
|
||||
for (const value of concept.values) {
|
||||
const valueName = value.concept && value.concept.name;
|
||||
const valueId = value.concept && value.concept.id;
|
||||
body.values.push({ label: value.label, targetName: valueName, targetId: valueId });
|
||||
}
|
||||
for (const relationship of concept.relationships) {
|
||||
body.relationships.push({ label: relationship.label, targetName: relationship.concept.name, targetId: relationship.concept.id });
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return response.end(JSON.stringify(body));
|
||||
}
|
||||
response.writeHead(404);
|
||||
return response.end('Concept not found');
|
||||
},
|
||||
'/instances': (request, response) => {
|
||||
const instances = [];
|
||||
for (const instance of this.node.instances) {
|
||||
instances.push({
|
||||
name: instance.name,
|
||||
id: instance.id,
|
||||
conceptName: instance.concept.name,
|
||||
conceptId: instance.concept.id,
|
||||
});
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify(instances));
|
||||
},
|
||||
'/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);
|
||||
}
|
||||
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: [],
|
||||
relationships: [],
|
||||
};
|
||||
for (const concept of instance.subConcepts) {
|
||||
body.subConcepts.push({ name: concept.name, id: concept.id });
|
||||
}
|
||||
for (const value of instance.values) {
|
||||
const valueName = value.instance.name || value.instance;
|
||||
const valueId = value.instance.id;
|
||||
const conceptName = value.instance.concept && value.instance.concept.name;
|
||||
const conceptId = value.instance.concept && value.instance.concept.id;
|
||||
body.values.push({ label: value.label, targetName: valueName, targetId: valueId, targetConceptName: conceptName, targetConceptId: conceptId });
|
||||
}
|
||||
for (const relationship of instance.relationships) {
|
||||
body.relationships.push({ label: relationship.label, targetName: relationship.instance.name, targetId: relationship.instance.id, targetConceptName: relationship.instance.concept.name, targetConceptId: relationship.instance.concept.id });
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return response.end(JSON.stringify(body));
|
||||
}
|
||||
response.writeHead(404);
|
||||
return response.end('Unable to find the instance.');
|
||||
},
|
||||
'/info': (request, response) => {
|
||||
const body = { recentInstances: [], recentConcepts: [], instanceCount: this.node.instances.length, conceptCount: this.node.concepts.length };
|
||||
const recentInstances = this.node.instances.slice(this.node.instances.length >= 10 ? this.node.instances.length - 10 : 0);
|
||||
for (const instance of recentInstances) {
|
||||
body.recentInstances.push({
|
||||
name: instance.name,
|
||||
id: instance.id,
|
||||
conceptName: instance.concept.name,
|
||||
conceptId: instance.concept.id,
|
||||
});
|
||||
}
|
||||
for (const concept of this.node.concepts) {
|
||||
body.recentConcepts.push({
|
||||
name: concept.name,
|
||||
id: concept.id,
|
||||
});
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify(body));
|
||||
},
|
||||
'/model': (request, response) => {
|
||||
let body = '';
|
||||
for (const concept of this.node.concepts) { body += `${concept.creationCE}\n`; }
|
||||
for (const concept of this.node.concepts) { body += `${concept.getCE(true)}\n`; }
|
||||
for (const instance of this.node.instances) { body += `${instance.creationCE}\n`; }
|
||||
for (const instance of this.node.instances) { body += `${instance.getCE(true)}\n`; }
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce', 'Content-Disposition': `attachment; filename="${this.node.agent.name}.ce"` });
|
||||
response.end(body);
|
||||
},
|
||||
},
|
||||
POST: {
|
||||
'/cards': (request, response) => {
|
||||
let body = '';
|
||||
request.on('data', (chunk) => { body += chunk; });
|
||||
request.on('end', () => {
|
||||
const ignores = body.split(/\\n|\n/);
|
||||
const agentRegex = decodeURIComponent(request.url).match(/agent=(.*)/);
|
||||
const agentStr = agentRegex ? agentRegex[1] : null;
|
||||
const agents = (agentStr && agentStr.toLowerCase().split(',')) || [];
|
||||
let s = '';
|
||||
for (const card of this.node.getInstances('card', true)) {
|
||||
if (ignores.indexOf(card.name) === -1) {
|
||||
if (agents.length === 0) {
|
||||
s += `${card.ce}\n`;
|
||||
} else if (card.is_tos) {
|
||||
for (const to of card.is_tos) {
|
||||
for (const agent of agents) {
|
||||
if (to.name.toLowerCase() === agent) {
|
||||
s += `${card.ce}\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce' });
|
||||
response.end(s);
|
||||
});
|
||||
},
|
||||
'/sentences': (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 = this.node.addSentences(sentences);
|
||||
response.writeHead(200, { 'Content-Type': 'text/ce' });
|
||||
response.end(responses.map(resp => resp.data).join('\n'));
|
||||
});
|
||||
},
|
||||
},
|
||||
PUT: {
|
||||
'/reset': (request, response) => {
|
||||
this.node.resetAll();
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
},
|
||||
'/agent/name': (request, response) => {
|
||||
let body = '';
|
||||
request.on('data', (chunk) => { body += chunk; });
|
||||
request.on('end', () => {
|
||||
body = decodeURIComponent(body.replace('name=', '').replace(/\+/g, ' '));
|
||||
this.node.agent.setName(body);
|
||||
response.writeHead(302, { Location: '/' });
|
||||
response.end();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
start() {
|
||||
this.server = http.createServer((request, response) => {
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
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}.`);
|
||||
}
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end(`404: Resource not found for method ${request.method}.`);
|
||||
}
|
||||
} else if (request.method === 'OPTIONS') {
|
||||
response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type');
|
||||
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
} else {
|
||||
response.writeHead(405);
|
||||
response.end('405: Method not allowed on this server.');
|
||||
}
|
||||
});
|
||||
this.server.listen(this.port);
|
||||
this.server.on('error', () => { this.node = undefined; });
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.server) {
|
||||
delete this.node;
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const name = process.argv.length > 2 ? process.argv[2] : 'Moira';
|
||||
const port = process.argv.length > 3 ? process.argv[3] : 5555;
|
||||
const models = process.argv.slice(4);
|
||||
new CEServer(name, port, models).start();
|
||||
}
|
||||
|
||||
module.exports = CEServer;
|
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* 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.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`;
|
||||
} 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.error && 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) {
|
||||
// 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.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;
|
||||
}
|
||||
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 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.error) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handle(card) {
|
||||
if (card.type.name in this.handlers && 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;
|
234
src/NLParser.js
@ -1,234 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
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
|
||||
* - 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 NLParser.success(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 NLParser.success(`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 NLParser.error(`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;
|
@ -1,257 +0,0 @@
|
||||
/*
|
||||
* 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.unsentGistCards = {};
|
||||
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) {
|
||||
if (!(policy.target.name in this.unsentTellCards)) {
|
||||
this.unsentTellCards[policy.target.name] = [];
|
||||
}
|
||||
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) {
|
||||
if (!(policy.target.name in this.unsentAskCards)) {
|
||||
this.unsentAskCards[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] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'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) {
|
||||
// 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) {
|
||||
if (card.timestamp && card.is_froms.length) {
|
||||
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.name, 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' && policy.type.name in this.handlers) {
|
||||
this.handlers[policy.type.name](policy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PolicyHandler;
|
@ -1,406 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
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
|
||||
* - 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) {
|
||||
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}.`);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return QuestionParser.success(`I don't know what ${thing} is.`);
|
||||
}
|
||||
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) {
|
||||
return QuestionParser.success(`I don't know where ${instance.name} is.`);
|
||||
}
|
||||
message = instance.name;
|
||||
for (const place in places) {
|
||||
if (place) {
|
||||
message += ` ${place}`;
|
||||
if (places[place] > 1) {
|
||||
message += ` (${places[place]} times)`;
|
||||
}
|
||||
message += ' and';
|
||||
}
|
||||
}
|
||||
return QuestionParser.success(`${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 QuestionParser.success(`${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 QuestionParser.success(`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 QuestionParser.success(`${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 QuestionParser.success(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 QuestionParser.success(`'${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]}'`;
|
||||
}
|
||||
} 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 QuestionParser.success(fuzzyGist);
|
||||
}
|
||||
return QuestionParser.success('I don\'t know who or what that is.');
|
||||
}
|
||||
return QuestionParser.success(concept.gist);
|
||||
}
|
||||
return QuestionParser.success(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 QuestionParser.success(`${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}.`);
|
||||
}
|
||||
} catch (err) {
|
||||
return QuestionParser.success('Sorry - I can\'t work out what you\'re asking.');
|
||||
}
|
||||
return QuestionParser.success('Sorry - I can\'t work out what you\'re asking about.');
|
||||
}
|
||||
|
||||
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();
|
||||
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) {
|
||||
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 QuestionParser.success(`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.');
|
||||
}
|
||||
|
||||
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.getInstances();
|
||||
s = 'All instances:';
|
||||
}
|
||||
if (ins.length === 0) {
|
||||
return QuestionParser.success('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(', ')}`);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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(' ')) {
|
||||
multipleSearch = searchFor.split(' ');
|
||||
}
|
||||
|
||||
if (multipleSearch) {
|
||||
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 {
|
||||
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;
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
for (const ruleInstance of this.node.getInstances('rule')) {
|
||||
const rule = RuleEngine.parseRule(ruleInstance.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.concept.ancestors;
|
||||
ancestorConcepts.push(objectInstance.concept);
|
||||
for (const ancestorConcept of ancestorConcepts) {
|
||||
if (ancestorConcept.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;
|
223
test/CEParser.js
@ -1,223 +0,0 @@
|
||||
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');
|
||||
});
|
||||
it('prevent multi-conceptualisation', () => {
|
||||
node.addCE('conceptualise a ~ river ~ R');
|
||||
node.addCE('conceptualise a ~ river ~ R');
|
||||
let counter = 0;
|
||||
for (const concept of node.concepts) {
|
||||
if (concept.name === 'river'){
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
expect(counter).to.equal(1);
|
||||
});
|
||||
it('ensure concepts can be addressed by synonyms', () => {
|
||||
node.addCE('conceptualise a ~ seat ~ that ~ is expressed by ~ chair and has the value V as ~ height ~');
|
||||
node.addCE('there is a chair named chair1 that has 43cm as height');
|
||||
expect(node.instances.chair1.height).to.equal('43cm');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('prevent multi-instantiation', () => {
|
||||
node.addCE('there is a person named Francesca');
|
||||
node.addCE('there is a person named Francesca');
|
||||
let counter = 0;
|
||||
for (const instance of node.instances) {
|
||||
if (instance.name === 'Francesca'){
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
expect(counter).to.equal(1);
|
||||
});
|
||||
it('ensure instance CE is correct', () => {
|
||||
node.addCE('there is an entity named Hagrid');
|
||||
const hagrid = node.instances.hagrid;
|
||||
expect(hagrid.ce).to.equal('there is a entity named \'Hagrid\'.');
|
||||
node.addCE('the entity Hagrid is a person');
|
||||
expect(hagrid.ce).to.equal('there is a entity named \'Hagrid\' that is a person.');
|
||||
});
|
||||
|
||||
it('ensure instances can be addressed by synonyms', () => {
|
||||
node.addCE('conceptualise an ~ engineer ~ E');
|
||||
node.addCE('there is a person named William that is expressed by Will');
|
||||
node.addCE('the person Will is an engineer');
|
||||
expect(node.instances.william.subConcepts[0].name).to.be('engineer');
|
||||
});
|
||||
it('ensure instances inherit properties from subConcepts', () => {
|
||||
node.addCE('conceptualise a ~ borough ~ B');
|
||||
node.addCE('conceptualise the londoner L ~ lives in ~ the borough B');
|
||||
node.addCE('conceptualise the barrister B has the value V as ~ speciality ~');
|
||||
node.addCE('there is a person named Amy that is a londoner and is a barrister');
|
||||
node.addCE('the person Amy lives in the borough Chelsea and has \'family law\' as speciality');
|
||||
expect(node.instances.amy.lives_in.name).to.be('Chelsea');
|
||||
expect(node.instances.amy.speciality).to.be('family law');
|
||||
});
|
||||
it('ensure strings with a mix of quoted and unquoted names/values are parsed', () => {
|
||||
node.addCE('there is a londoner named Ella that lives in the borough \'Kensington and Chelsea\'');
|
||||
node.addCE('there is a londoner named \'Betty Hughes\' that lives in the borough Camden');
|
||||
node.addCE('there is a londoner named Sally');
|
||||
node.addCE('the londoner Sally lives in the borough \'Kensington and Chelsea\'');
|
||||
expect(node.instances.ella.lives_in.name).to.be('Kensington and Chelsea');
|
||||
expect(node.instances.betty_hughes.lives_in.name).to.be('Camden');
|
||||
expect(node.instances.sally.lives_in.name).to.be('Kensington and Chelsea');
|
||||
});
|
||||
});
|
||||
|
||||
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 and has the city Cardiff as address.', function() {
|
||||
node.addCE('conceptualise a ~ company ~ C');
|
||||
node.addCE('conceptualise a ~ city ~ 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 ~ and has the city C as ~ address ~');
|
||||
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 and has the city Cardiff as address.');
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|