├── .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 | --------------------------------------------------------------------------------