├── .gitignore
├── LICENSE
├── README.md
├── examples
├── multiple.js
├── repl.js
└── simple.js
├── index.js
├── package.json
└── src
├── Engine.js
├── EngineBuilder.js
├── Entity.js
├── Intent.js
├── RegexEntity.js
└── worker.py
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | virtualenv/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # adaptjs
2 |
3 | A nodejs wrapper around the [Adapt Intent Parser from Mycroft](https://github.com/MycroftAI/adapt), which converts natural language into structured intents based on intent definitions.
4 |
5 | ## Installation
6 |
7 | ```
8 | $ npm install --save adaptjs
9 | ```
10 |
11 | Install [MycroftAI/adapt](https://github.com/MycroftAI/adapt) as described in their [README](https://github.com/MycroftAI/adapt).
12 |
13 | ## Example
14 |
15 | ```javascript
16 | "use strict";
17 | const EngineBuilder = require("adaptjs").EngineBuilder;
18 |
19 | let builder = new EngineBuilder();
20 |
21 | builder.entity("WeatherKeyword", ["weather"]);
22 | builder.entity("WeatherType", ["snow", "rain", "wind", "sleet", "sun"]);
23 | builder.entity("Location", ["Seattle", "San Francisco", "Tokyo"]);
24 |
25 | builder.intent("WeatherIntent")
26 | .require("WeatherKeyword", "weatherkey")
27 | .optionally("WeatherType")
28 | .require("Location");
29 |
30 | let engine = builder.build();
31 |
32 | engine.query("Whats the weather in San Francisco today?")
33 | .then(intents => { console.log(intents); engine.stop(); })
34 | .catch(error => { console.log(error); console.log(error.stack); engine.stop(); });
35 |
36 | /* [ { intent_type: 'WeatherIntent',
37 | weatherkey: 'weather',
38 | Location: 'San Francisco',
39 | confidence: 0.4878048780487805,
40 | target: null } ] */
41 | ```
42 |
43 | See [examples/](https://github.com/hinzundcode/adaptjs/tree/master/examples) for more examples of the API and [MycroftAI/adapt](https://github.com/MycroftAI/adapt) for more information about the Adapt Intent Parser itself.
44 |
45 | ## API
46 |
47 | ```javascript
48 | new EngineBuilder([adaptInstallationPath]): EngineBuilder
49 | EngineBuilder.entity(name, arrayOfValues): Entity
50 | EngineBuilder.regexEntity(pattern): RegexEntity
51 | EngineBuilder.intent(name): Intent
52 | EngineBuilder.build(): Engine
53 |
54 | Intent.require(entity, [attributeName]): this
55 | Intent.optionally(entity, [attributeName]): this
56 |
57 | Engine.start()
58 | Engine.stop()
59 | Engine.isRunning(): bool
60 | Engine.query(input): Promise
61 | ```
62 |
63 | ## How it works
64 |
65 | The EngineBuilder on the JavaScript side creates a definition of all entities and intents and passes it to a python child process as JSON. The python child process keeps running in the background and receives new input over stdin. You have to stop the child process manually using `engine.stop()`.
66 |
67 | If you have installed Adapt in a specific location (not your current-working-directory), then you can pass that path as the first argument of the EngineBuilder. This path should be absolute, because otherwise it will be relative out of node_modules/ which _will_ lead to problems later on.
68 |
--------------------------------------------------------------------------------
/examples/multiple.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const EngineBuilder = require("..").EngineBuilder;
3 | const readline = require("readline");
4 |
5 | let builder = new EngineBuilder();
6 |
7 | builder.entity("WeatherKeyword", ["weather"]);
8 | builder.entity("WeatherType", ["snow", "rain", "wind", "sleet", "sun"]);
9 | builder.entity("Location", ["Seattle", "San Francisco", "Tokyo"]);
10 | //builder.regexEntity("in (?P.*)");
11 |
12 | builder.intent("WeatherIntent")
13 | .require("WeatherKeyword", "weatherkey")
14 | .optionally("WeatherType")
15 | .require("Location");
16 |
17 | builder.entity("Artist", ["third eye blind", "the who", "the clash", "john mayer", "kings of leon", "adelle"]);
18 | builder.entity("MusicVerb", ["listen", "hear", "play"]);
19 | builder.entity("MusicKeyword", ["songs", "music"]);
20 |
21 | builder.intent("MusicIntent")
22 | .require("MusicVerb")
23 | .optionally("MusicKeyword")
24 | .optionally("Artist");
25 |
26 | let engine = builder.build();
27 |
28 | engine.query("weather in seattle").then(intents => {
29 | console.log("seattle", intents);
30 | }).catch(error => {
31 | console.log(error.stack);
32 | });
33 |
34 | engine.query("play the who").then(intents => {
35 | console.log("who", intents);
36 | }).catch(error => {
37 | console.log(error.stack);
38 | });
39 |
40 | engine.query("weather in san francisco").then(intents => {
41 | console.log("san francisco", intents);
42 | }).catch(error => {
43 | console.log(error.stack);
44 | });
45 |
--------------------------------------------------------------------------------
/examples/repl.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const EngineBuilder = require("..").EngineBuilder;
3 | const readline = require("readline");
4 |
5 | let builder = new EngineBuilder();
6 |
7 | builder.entity("WeatherKeyword", ["weather"]);
8 | builder.entity("WeatherType", ["snow", "rain", "wind", "sleet", "sun"]);
9 | builder.entity("Location", ["Seattle", "San Francisco", "Tokyo"]);
10 | //builder.regexEntity("in (?P.*)");
11 |
12 | builder.intent("WeatherIntent")
13 | .require("WeatherKeyword", "weatherkey")
14 | .optionally("WeatherType")
15 | .require("Location");
16 |
17 | builder.entity("Artist", ["third eye blind", "the who", "the clash", "john mayer", "kings of leon", "adelle"]);
18 | builder.entity("MusicVerb", ["listen", "hear", "play"]);
19 | builder.entity("MusicKeyword", ["songs", "music"]);
20 |
21 | builder.intent("MusicIntent")
22 | .require("MusicVerb")
23 | .optionally("MusicKeyword")
24 | .optionally("Artist");
25 |
26 | let engine = builder.build();
27 |
28 | const rl = readline.createInterface({
29 | input: process.stdin,
30 | output: process.stdout,
31 | });
32 | rl.setPrompt("> ");
33 | rl.prompt();
34 |
35 | rl.on("line", line => {
36 | engine.query(line).then(intents => {
37 | console.log(intents);
38 | rl.prompt();
39 | }).catch(error => {
40 | console.log(error);
41 | console.log(error.stack);
42 | rl.prompt();
43 | });
44 | });
45 |
46 | rl.on("close", () => {
47 | engine.stop();
48 | });
49 |
--------------------------------------------------------------------------------
/examples/simple.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const EngineBuilder = require("..").EngineBuilder;
3 |
4 | let builder = new EngineBuilder();
5 |
6 | builder.entity("WeatherKeyword", ["weather"]);
7 | builder.entity("WeatherType", ["snow", "rain", "wind", "sleet", "sun"]);
8 | builder.entity("Location", ["Seattle", "San Francisco", "Tokyo"]);
9 |
10 | builder.intent("WeatherIntent")
11 | .require("WeatherKeyword", "weatherkey")
12 | .optionally("WeatherType")
13 | .require("Location");
14 |
15 | let engine = builder.build();
16 |
17 | engine.query("Whats the weather in San Francisco today?")
18 | .then(intents => { console.log(intents); engine.stop(); })
19 | .catch(error => { console.log(error); console.log(error.stack); engine.stop(); });
20 |
21 | /*[ { intent_type: 'WeatherIntent',
22 | weatherkey: 'weather',
23 | Location: 'San Francisco',
24 | confidence: 0.4878048780487805,
25 | target: null } ]*/
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const EngineBuilder = require("./src/EngineBuilder");
3 | const Engine = require("./src/Engine");
4 | const Entity = require("./src/Entity");
5 | const RegexEntity = require("./src/RegexEntity");
6 | const Intent = require("./src/Intent");
7 |
8 | module.exports = {
9 | EngineBuilder: EngineBuilder,
10 | Engine: Engine,
11 | Entity: Entity,
12 | RegexEntity: RegexEntity,
13 | Intent: Intent,
14 | };
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adaptjs",
3 | "description": "a javascript wrapper around MycroftAI/adapt",
4 | "version": "1.0.3",
5 | "license": "LGPL-3.0",
6 | "main": "./index.js",
7 | "author": {
8 | "name": "hinzundcode",
9 | "email": "schokoc+git@appucino.de"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git://github.com/hinzundcode/adaptjs.git"
14 | },
15 | "keywords": [
16 | "natural",
17 | "language",
18 | "parser",
19 | "parsing",
20 | "intent"
21 | ],
22 | "dependencies": {
23 | "debug": "^2.2.0",
24 | "json-stream": "^1.0.0"
25 | },
26 | "engines": {
27 | "node": ">=5.11"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Engine.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const debug = require("debug")("adaptjs");
3 | const spawn = require("child_process").spawn;
4 | const JSONStream = require("json-stream");
5 |
6 | class Engine {
7 | constructor(schema, adaptInstallationPath) {
8 | this.schema = schema;
9 | this.adaptInstallationPath = adaptInstallationPath;
10 | this.worker = null;
11 | this.stream = JSONStream();
12 | this.queue = [];
13 | this.currentJob = null;
14 |
15 | this.stream.on("data", (result) => {
16 | if (!this.currentJob) return;
17 |
18 | this.currentJob.resolve(result.intents);
19 | this.currentJob = null;
20 | this.process();
21 | });
22 | }
23 |
24 | start() {
25 | if (this.worker) return;
26 | this.worker = spawn("python", [__dirname+"/worker.py", JSON.stringify(this.schema), this.adaptInstallationPath]);
27 | debug("worker spawned");
28 |
29 | this.worker.on("error", err => {
30 | debug("worker error");
31 | debug(err);
32 |
33 | this.stop(err);
34 | });
35 | this.worker.on("exit", (code, signal) => {
36 | debug("worker exited");
37 | debug(code);
38 | debug(signal);
39 |
40 | this.stop(new Error("worker exited"));
41 | });
42 |
43 | this.worker.stdout.pipe(this.stream);
44 | //this.worker.stdout.pipe(process.stdout);
45 |
46 | if (debug.enabled)
47 | this.worker.stderr.pipe(process.stderr);
48 | }
49 |
50 | stop(error) {
51 | if (!error)
52 | error = new Error("worker stop");
53 |
54 | if (this.worker)
55 | this.worker.kill();
56 |
57 | this.worker = null;
58 |
59 | if (this.currentJob)
60 | this.currentJob.reject(error);
61 |
62 | for (let job of this.queue)
63 | job.reject(error);
64 |
65 | this.currentJob = null;
66 | this.queue = [];
67 | }
68 |
69 | isRunning() {
70 | return this.worker != null;
71 | }
72 |
73 | process() {
74 | if (this.currentJob) return;
75 |
76 | let job = this.queue.shift();
77 | if (!job) return;
78 | this.currentJob = job;
79 |
80 | this.start();
81 | let request = { input: job.input };
82 | this.worker.stdin.write(JSON.stringify(request)+"\n");
83 | }
84 |
85 | query(input) {
86 | return new Promise((resolve, reject) => {
87 | this.queue.push({
88 | input: input,
89 | resolve: resolve,
90 | reject: reject,
91 | });
92 | this.process();
93 | });
94 | }
95 | }
96 |
97 | module.exports = Engine;
98 |
--------------------------------------------------------------------------------
/src/EngineBuilder.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const Entity = require("./Entity");
3 | const RegexEntity = require("./RegexEntity");
4 | const Intent = require("./Intent");
5 | const Engine = require("./Engine");
6 |
7 | class EngineBuilder {
8 | constructor(adaptInstallationPath) {
9 | this.entities = [];
10 | this.intents = [];
11 | this.adaptInstallationPath = adaptInstallationPath;
12 | }
13 |
14 | entity(name, values) {
15 | let entity = new Entity(name, values);
16 | this.entities.push(entity);
17 | return entity;
18 | }
19 |
20 | regexEntity(pattern) {
21 | let entity = new RegexEntity(pattern);
22 | this.entities.push(entity);
23 | return entity;
24 | }
25 |
26 | intent(name) {
27 | let intent = new Intent(name);
28 | this.intents.push(intent);
29 | return intent;
30 | }
31 |
32 | build() {
33 | return new Engine({
34 | entities: this.entities.map(entity => entity.encode()),
35 | intents: this.intents.map(intent => intent.encode())
36 | }, this.adaptInstallationPath);
37 | }
38 | }
39 |
40 | module.exports = EngineBuilder;
41 |
--------------------------------------------------------------------------------
/src/Entity.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Entity {
4 | constructor(name, values) {
5 | this.name = name;
6 | this.values = values;
7 | }
8 |
9 | encode() {
10 | return {
11 | type: "string",
12 | name: this.name,
13 | values: this.values,
14 | };
15 | }
16 | }
17 |
18 | module.exports = Entity;
19 |
--------------------------------------------------------------------------------
/src/Intent.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Intent {
4 | constructor(name) {
5 | this.name = name;
6 | this.requirements = [];
7 | this.optionals = [];
8 | }
9 |
10 | require(entity, attribute) {
11 | if (!attribute) attribute = entity;
12 | this.requirements.push({ entity: entity, attribute: attribute });
13 | return this;
14 | }
15 |
16 | optionally(entity, attribute) {
17 | if (!attribute) attribute = entity;
18 | this.optionals.push({ entity: entity, attribute: attribute });
19 | return this;
20 | }
21 |
22 | encode() {
23 | return {
24 | name: this.name,
25 | requirements: this.requirements,
26 | optionals: this.optionals,
27 | };
28 | }
29 | }
30 |
31 | module.exports = Intent;
32 |
--------------------------------------------------------------------------------
/src/RegexEntity.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class RegexEntity {
4 | constructor(pattern) {
5 | this.pattern = pattern;
6 | }
7 |
8 | encode() {
9 | return {
10 | type: "regex",
11 | pattern: this.pattern,
12 | };
13 | }
14 | }
15 |
16 | module.exports = RegexEntity;
17 |
--------------------------------------------------------------------------------
/src/worker.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 |
4 | # If there's a second argument given, use that to insert an import path
5 | # This enables users to use their own Adapt installation directories.
6 | if len(sys.argv) > 2:
7 | sys.path.insert(0, sys.argv[2])
8 |
9 | from adapt.intent import IntentBuilder
10 | from adapt.engine import IntentDeterminationEngine
11 |
12 | engine = IntentDeterminationEngine()
13 |
14 | schema = json.loads(sys.argv[1])
15 |
16 | for entity in schema["entities"]:
17 | if entity["type"] == "string":
18 | for value in entity["values"]:
19 | engine.register_entity(value, entity["name"])
20 | elif entity["type"] == "regex":
21 | engine.register_regex_entity(entity["pattern"])
22 |
23 | for intent in schema["intents"]:
24 | ib = IntentBuilder(intent["name"].encode("utf-8"))
25 | for requirement in intent["requirements"]:
26 | ib.require(requirement["entity"], requirement["attribute"])
27 | for optional in intent["optionals"]:
28 | ib.optionally(optional["entity"], optional["attribute"])
29 | engine.register_intent_parser(ib.build())
30 |
31 | if __name__ == "__main__":
32 | while True:
33 | line = sys.stdin.readline()
34 | query = json.loads(line)
35 | intents = list(engine.determine_intent(query["input"]))
36 | response = {"intents": intents}
37 | print(json.dumps(response))
38 | sys.stdout.flush()
39 |
--------------------------------------------------------------------------------