├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── cli.js ├── index.js ├── package.json ├── rollup.config.js ├── samples ├── CaffeShop │ ├── botium.json │ ├── botium.local1.json │ ├── package.json │ └── spec │ │ ├── botium.spec.js │ │ └── convo │ │ └── follow_up_intent.convo.txt ├── RoomReservation │ ├── botium.json │ ├── package.json │ └── spec │ │ ├── botium.spec.js │ │ └── convo │ │ ├── audio.convo.txt │ │ ├── bookroom.wav │ │ ├── buttons.convo.txt │ │ └── context.convo.txt ├── Voice │ ├── botium.json │ ├── package.json │ └── spec │ │ ├── botium.spec.js │ │ └── convo │ │ ├── audio.convo.txt │ │ └── bookroom.wav ├── assistant │ ├── botium.json │ ├── package.json │ └── spec │ │ ├── botium.spec.js │ │ └── convo │ │ ├── basiccard.convo.txt │ │ ├── carousel.convo.txt │ │ ├── linkoutsuggestion.convo.txt │ │ ├── list.convo.txt │ │ ├── simpleresponse.convo.txt │ │ └── suggestions.convo.txt └── banking │ ├── .gitignore │ ├── botium.json │ ├── package.json │ └── spec │ ├── botium.spec.js │ └── convo │ ├── asserters.convo.txt │ ├── incomprehension.convo.txt │ └── transfer.convo.txt ├── src ├── dialogflowintents.js ├── helpers.js └── nlp.js └── structJson.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "standard" 3 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | package-lock.json 65 | /.idea 66 | package-lock.json 67 | *.local.json 68 | **/botiumwork/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Botium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Botium Connector for Google Dialogflow 2 | 3 | [![NPM](https://nodei.co/npm/botium-connector-dialogflow.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/botium-connector-dialogflow/) 4 | 5 | [![npm version](https://badge.fury.io/js/botium-connector-dialogflow.svg)](https://badge.fury.io/js/botium-connector-dialogflow) 6 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)]() 7 | 8 | This is a [Botium](https://github.com/codeforequity-at/botium-core) connector for testing your Dialogflow Agents. 9 | 10 | __Did you read the [Botium in a Nutshell](https://medium.com/@floriantreml/botium-in-a-nutshell-part-1-overview-f8d0ceaf8fb4) articles ? Be warned, without prior knowledge of Botium you won't be able to properly use this library!__ 11 | 12 | ## How it works ? 13 | Botium runs your conversations against the Dialogflow API. 14 | 15 | It can be used as any other Botium connector with all Botium Stack components: 16 | * [Botium CLI](https://github.com/codeforequity-at/botium-cli/) 17 | * [Botium Bindings](https://github.com/codeforequity-at/botium-bindings/) 18 | * [Botium Box](https://www.botium.ai) 19 | 20 | Extracts Button, Media, Card, and NLP information (intent, entities) from Chatbot API response. Accordingly it is possible to use a corresponding [Botium Asserter](https://botium-docs.readthedocs.io/en/latest/05_botiumscript/index.html#using-asserters). 21 | 22 | ## Requirements 23 | 24 | * __Node.js and NPM__ 25 | * a __Dialogflow__ agent, and user account with administrative rights 26 | * a __project directory__ on your workstation to hold test cases and Botium configuration 27 | 28 | ## Install Botium and Dialogflow Connector 29 | 30 | When using __Botium CLI__: 31 | 32 | ``` 33 | > npm install -g botium-cli 34 | > npm install -g botium-connector-dialogflow 35 | > botium-cli init 36 | > botium-cli run 37 | ``` 38 | 39 | When using __Botium Bindings__: 40 | 41 | ``` 42 | > npm install -g botium-bindings 43 | > npm install -g botium-connector-dialogflow 44 | > botium-bindings init mocha 45 | > npm install && npm run mocha 46 | ``` 47 | 48 | When using __Botium Box__: 49 | 50 | _Already integrated into Botium Box, no setup required_ 51 | 52 | ## Connecting Dialogflow Agent to Botium 53 | 54 | Open the file _botium.json_ in your working directory and add the Google credentials for accessing your Dialogflow agent. [This article](https://wiki.botiumbox.com/technical-reference/botium-connectors/supported-technologies/botium-connector-dialogflow/) shows how to retrieve all those settings. 55 | 56 | ``` 57 | { 58 | "botium": { 59 | "Capabilities": { 60 | "PROJECTNAME": "", 61 | "CONTAINERMODE": "dialogflow", 62 | "DIALOGFLOW_PROJECT_ID": "", 63 | "DIALOGFLOW_CLIENT_EMAIL": "", 64 | "DIALOGFLOW_PRIVATE_KEY": "" 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | To check the configuration, run the emulator (Botium CLI required) to bring up a chat interface in your terminal window: 71 | 72 | ``` 73 | > botium-cli emulator 74 | ``` 75 | 76 | Botium setup is ready, you can begin to write your [BotiumScript](https://botium-docs.readthedocs.io/en/latest/05_botiumscript/index.html#) files. 77 | 78 | ## Using the botium-connector-dialogflow-cli 79 | 80 | This connector provides a CLI interface for importing convos and utterances from your Dialogflow agent and convert it to BotiumScript. 81 | 82 | * Intents and Utterances are converted to BotiumScript utterances files (using the _--buildconvos_ option) 83 | * Conversations are reverse engineered and converted to BotiumScript convo files (using the _--buildmultistepconvos_ option) 84 | 85 | You can either run the CLI with *[botium-cli](https://github.com/codeforequity-at/botium-cli) (recommended - it is integrated there)*, or directly from this connector (see samples/assistant directory for some examples): 86 | 87 | > botium-connector-dialogflow-cli import --buildconvos 88 | > botium-connector-dialogflow-cli import --buildmultistepconvos 89 | 90 | _Please note that you will have to install the npm packages botium-core manually before using this CLI_ 91 | 92 | For getting help on the available CLI options and switches, run: 93 | 94 | > botium-connector-dialogflow-cli import --help 95 | 96 | ## Dialogflow Context Handling 97 | 98 | When using BotiumScript, you can do assertions on and manipulation of the [Dialogflow context variables](https://cloud.google.com/dialogflow/docs/contexts-overview). 99 | 100 | ### Asserting context and context parameters 101 | 102 | For asserting existance of context variables, you can use the [JSON_PATH asserter](https://botium-docs.readthedocs.io/en/latest/05_botiumscript/index.html#jsonpath-asserter): 103 | 104 | **Assert output context name** 105 | 106 | #bot 107 | JSON_PATH $.outputContexts[0].name|*testsession* 108 | 109 | _Use the ***** for wildcard matching_ 110 | 111 | **Assert context parameter "myparameter" for output context named "mycontext"** 112 | 113 | #bot 114 | JSON_PATH $.outputContexts[?(@.name.indexOf('mycontext') >= 0)].parameters.myparameter|somevalue 115 | 116 | _Use the JSONPath filer for matching a context by name instead of index_ 117 | 118 | **Assert lifespan for output context named "mycontext"** 119 | 120 | #bot 121 | JSON_PATH $.outputContexts[?(@.name.indexOf('mycontext') >= 0 && @.lifespanCount > 2)] 122 | 123 | ### Adding context variables 124 | 125 | For adding a context variable, you have to use the [UPDATE_CUSTOM logic hook](https://botium-docs.readthedocs.io/en/latest/05_botiumscript/index.html#update-custom). This example will set two context variables, one with some parameters: 126 | 127 | #me 128 | heyo 129 | UPDATE_CUSTOM SET_DIALOGFLOW_CONTEXT|mycontext1|7 130 | UPDATE_CUSTOM SET_DIALOGFLOW_CONTEXT|mycontext2|{"lifespan": 4, "parameters": { "test": "test1"}} 131 | 132 | The parameters are: 133 | 1. SET_DIALOGFLOW_CONTEXT 134 | 2. The name of the context variable (if already existing, it will be overwritten) 135 | 3. The lifespan of the context variable (if scalar value), or the lifespan and the context parameters (if JSON formatted) 136 | 137 | ## Dialogflow Query Parameters 138 | 139 | When using BotiumScript, you can do manipulation of the [Dialogflow query parameters](https://cloud.google.com/dialogflow/docs/reference/rest/v2beta1/QueryParameters).You have to use the [UPDATE_CUSTOM logic hook](https://botium-docs.readthedocs.io/en/latest/05_botiumscript/index.html#update-custom). This example will add a _payload_ field with some JSON content in the query parameters: 140 | 141 | #me 142 | heyo 143 | UPDATE_CUSTOM SET_DIALOGFLOW_QUERYPARAMS|payload|{"key": "value"} 144 | 145 | ## Supported Capabilities 146 | 147 | Set the capability __CONTAINERMODE__ to __dialogflow__ to activate this connector. 148 | 149 | ### DIALOGFLOW_PROJECT_ID 150 | 151 | Google project id. See [This article](https://chatbotsmagazine.com/3-steps-setup-automated-testing-for-google-assistant-and-dialogflow-de42937e57c6) 152 | 153 | ### DIALOGFLOW_ENVIRONMENT 154 | 155 | Dialogflow publishing environment name. See [This article](https://cloud.google.com/dialogflow/docs/agents-versions) 156 | 157 | ### DIALOGFLOW_CLIENT_EMAIL 158 | _Optional_ 159 | 160 | Google client email. See [This article](https://chatbotsmagazine.com/3-steps-setup-automated-testing-for-google-assistant-and-dialogflow-de42937e57c6) 161 | 162 | If not given, [Google default authentication](https://cloud.google.com/docs/authentication/getting-started) will be used. 163 | 164 | ### DIALOGFLOW_PRIVATE_KEY 165 | _Optional_ 166 | 167 | Google private key. See [This article](https://chatbotsmagazine.com/3-steps-setup-automated-testing-for-google-assistant-and-dialogflow-de42937e57c6) 168 | 169 | If not given, [Google default authentication](https://cloud.google.com/docs/authentication/getting-started) will be used. 170 | 171 | ### DIALOGFLOW_LANGUAGE_CODE 172 | 173 | The language of this conversational query. See [all languages](https://dialogflow.com/docs/reference/language). 174 | A Dialogflow Agent is multilingiual, Connector is not. But you can use more botium.json for each language. 175 | (Botium Box, or Botium CLI is recommended in this case. Botium Bindings does not support more botium.xml) 176 | 177 | ### DIALOGFLOW_OUTPUT_PLATFORM 178 | 179 | Set the chat platform to get platform dependent response. See [all platforms](https://dialogflow.com/docs/reference/message-objects#text_response_2) 180 | If you have multi platform dependent conversation, then it is the same situation as DIALOGFLOW_LANGUAGE_CODE 181 | 182 | ### DIALOGFLOW_FORCE_INTENT_RESOLUTION 183 | 184 | Experimental capability. 185 | 186 | From a Dialogflow response the Connector can extract zero, one, or more messages. Every message will got the NLP information like intent and entities from the Dialogflow response. 187 | If Connector extracts zero messages, then creates a dummy one, to hold the NLP information. With this flag you can turn off this feature. 188 | 189 | Default _true_ 190 | 191 | ### DIALOGFLOW_BUTTON_EVENTS 192 | Default _true_ 193 | 194 | Botium simulates button clicks by using [Dialogflow "Events"](https://dialogflow.com/docs/events). If the payload of the button click simulation is valid JSON, it should include a ["name" and a "parameters" attribute](https://cloud.google.com/dialogflow-enterprise/docs/reference/rpc/google.cloud.dialogflow.v2#google.cloud.dialogflow.v2.EventInput), otherwise the named event without parameters is triggered. 195 | 196 | By setting this capability to _false_ this behaviour can be disabled and a button click is sent as text input to Dialogflow. 197 | 198 | ### DIALOGFLOW_QUERY_PARAMS 199 | 200 | Setting the initial [Dialogflow query parameters](https://cloud.google.com/dialogflow/docs/reference/rest/v2beta1/QueryParameters). 201 | 202 | Has to be a JSON-string or JSON-object. 203 | 204 | ### DIALOGFLOW_INPUT_CONTEXT_NAME(_X) 205 | 206 | You can use [Contexts](https://dialogflow.com/docs/contexts). They can be useful if you dont want to start the conversation from beginning, 207 | or you can set a context parameter “testmode” to make the web api behind the fulfillment react in a different way than in normal mode. 208 | 209 | If you are using more context parameters then you have to use more Capabilities. Use a name, or number as suffix to distinguish them. (Like DIALOGFLOW_INPUT_CONTEXT_NAME_EMAIL). 210 | 211 | This Capability contains the name of the parameter. 212 | 213 | See also the [Sample botium.json](./samples/RoomReservation/botium.json) 214 | 215 | ### DIALOGFLOW_INPUT_CONTEXT_LIFESPAN(_X) 216 | 217 | The number of queries this parameter will remain active after being invoked. 218 | 219 | Mandatory Capability. 220 | 221 | ### DIALOGFLOW_INPUT_CONTEXT_PARAMETERS(_X) 222 | 223 | This Capability contains the values of the parameter. It is a JSON structure. See [Sample botium.json](./samples/RoomReservation/botium.json) 224 | 225 | Optional Capability. 226 | 227 | ### DIALOGFLOW_ENABLE_KNOWLEDGEBASE 228 | _Default: false_ 229 | 230 | This Capability enables support for [Dialogflow Knowledge Connectors](https://cloud.google.com/dialogflow/docs/knowledge-connectors). If this is set to _true_, then all knowledge bases connected to your Dialogflow agent are included in the queries. You can select individual knowledge bases by using a JSON array with the full knowledge base names, including the google project id and the knowledge base id: 231 | 232 | ... 233 | "DIALOGFLOW_ENABLE_KNOWLEDGEBASE": [ "projects/project-id/knowledgeBases/knowledge-base-id" ] 234 | ... 235 | 236 | ### DIALOGFLOW_FALLBACK_INTENTS 237 | _Default: ['Default Fallback Intent']_ 238 | 239 | As default the not recognized utterance will be categorized as _Default Fallback Intent_ by Dialogflow. 240 | If you change this behavior, you can inform connector about it. Used just for analyzation. 241 | 242 | ## Additional Capabilities for NLP Analytics 243 | 244 | The recommendation is to separate the Dialogflow agent you are using for NLP analytics from the one used for training your chatbot. There is a separate set of capabilities for connecting to an additional Dialogflow agent. 245 | 246 | ### DIALOGFLOW_NLP_PROJECT_ID 247 | 248 | Google project id. 249 | 250 | ### DIALOGFLOW_NLP_CLIENT_EMAIL 251 | 252 | Google client email. 253 | 254 | ### DIALOGFLOW_NLP_PRIVATE_KEY 255 | 256 | Google private key. 257 | 258 | ## Additional Capabilities for Audio Input (Speech Recognition) 259 | 260 | For more details about audio configuration, go [here](https://cloud.google.com/speech-to-text/docs/encoding?hl=de). 261 | 262 | ### DIALOGFLOW_AUDIOINPUT_ENCODING 263 | 264 | Audio File Encoding 265 | 266 | ### DIALOGFLOW_AUDIOINPUT_SAMPLERATEHERTZ 267 | 268 | Audio File Sample Rate in Hertz 269 | 270 | ### DIALOGFLOW_AUDIOINPUT_CHANNELS 271 | 272 | Audio File - Count of Channels 273 | 274 | ### DIALOGFLOW_AUDIOINPUT_RECOGNITION_PER_CHANNEL 275 | 276 | Audio File - Separate Recognition per Channel 277 | 278 | ### DIALOGFLOW_API_ENDPOINT 279 | 280 | By default the Dialogflow connector only works with the US (Global) region of Dialogflow, however it can be configured to connect to a region specific version of Dialogflow. This requires setting the project ID to include the location AND setting a custom API Endpoint for the specific region dialogflow instance. In this example configuration, an example of the dialogflow region would be `australia-southeast1`. List of available regions: https://cloud.google.com/dialogflow/es/docs/how/region 281 | 282 | ``` 283 | { 284 | "botium": { 285 | "Capabilities": { 286 | "PROJECTNAME": "", 287 | "CONTAINERMODE": "dialogflow", 288 | "DIALOGFLOW_PROJECT_ID": "/locations/", 289 | "DIALOGFLOW_CLIENT_EMAIL": "", 290 | "DIALOGFLOW_PRIVATE_KEY": "", 291 | "DIALOGFLOW_API_ENDPOINT": "-dialogflow.googleapis.com" 292 | } 293 | } 294 | } 295 | ``` 296 | 297 | ## Open Issues and Restrictions 298 | * Account Linking is not supported (Consider using [Botium Connector for Google Assistant](https://github.com/codeforequity-at/botium-connector-google-assistant) if you want to test it) 299 | * Not [all](https://cloud.google.com/dialogflow-enterprise/docs/reference/rest/v2/projects.agent.intents#Message) dialogflow response is supported, just 300 | * Text, 301 | * Image 302 | * Quick replies 303 | * Cards (You see cards as texts, images, and buttons) 304 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const yargsCmd = require('yargs') 3 | const slug = require('slug') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const mkdirp = require('mkdirp') 7 | const { BotDriver } = require('botium-core') 8 | 9 | const { importHandler, importArgs } = require('../src/dialogflowintents') 10 | const { exportHandler, exportArgs } = require('../src/dialogflowintents') 11 | 12 | const writeConvo = (compiler, convo, outputDir) => { 13 | const filename = path.resolve(outputDir, slug(convo.header.name) + '.convo.txt') 14 | 15 | mkdirp.sync(outputDir) 16 | 17 | const scriptData = compiler.Decompile([convo], 'SCRIPTING_FORMAT_TXT') 18 | 19 | fs.writeFileSync(filename, scriptData) 20 | return filename 21 | } 22 | 23 | const writeUtterances = (compiler, utterance, samples, outputDir) => { 24 | const filename = path.resolve(outputDir, slug(utterance) + '.utterances.txt') 25 | 26 | mkdirp.sync(outputDir) 27 | 28 | const scriptData = [utterance, ...samples].join('\n') 29 | 30 | fs.writeFileSync(filename, scriptData) 31 | return filename 32 | } 33 | 34 | yargsCmd.usage('Botium Connector Dialogflow CLI\n\nUsage: $0 [options]') // eslint-disable-line 35 | .help('help').alias('help', 'h') 36 | .version('version', require('../package.json').version).alias('version', 'V') 37 | .showHelpOnFail(true) 38 | .strict(true) 39 | .command({ 40 | command: 'import', 41 | describe: 'Importing Convos and Utterances from Dialogflow to Botium', 42 | builder: (yargs) => { 43 | for (const arg of Object.keys(importArgs)) { 44 | if (importArgs[arg].skipCli) continue 45 | yargs.option(arg, importArgs[arg]) 46 | } 47 | yargs.option('output', { 48 | describe: 'Output directory', 49 | type: 'string', 50 | default: '.' 51 | }) 52 | }, 53 | handler: async (argv) => { 54 | const outputDir = argv.output 55 | 56 | let convos = [] 57 | let utterances = [] 58 | try { 59 | ({ convos, utterances } = await importHandler(argv)) 60 | } catch (err) { 61 | console.log(`FAILED: ${err.message}`) 62 | return 63 | } 64 | 65 | const driver = new BotDriver() 66 | const compiler = await driver.BuildCompiler() 67 | 68 | for (const convo of convos) { 69 | try { 70 | const filename = writeConvo(compiler, convo, outputDir) 71 | console.log(`SUCCESS: wrote convo to file ${filename}`) 72 | } catch (err) { 73 | console.log(`WARNING: writing convo "${convo.header.name}" failed: ${err.message}`) 74 | } 75 | } 76 | for (const utterance of utterances) { 77 | try { 78 | const filename = writeUtterances(compiler, utterance.name, utterance.utterances, outputDir) 79 | console.log(`SUCCESS: wrote utterances to file ${filename}`) 80 | } catch (err) { 81 | console.log(`WARNING: writing utterances "${utterance.name}" failed: ${err.message}`) 82 | } 83 | } 84 | } 85 | }) 86 | .command({ 87 | command: 'export', 88 | describe: 'Uploading Utterances from Botium to Dialogflow', 89 | builder: (yargs) => { 90 | for (const arg of Object.keys(exportArgs)) { 91 | if (exportArgs[arg].skipCli) continue 92 | yargs.option(arg, exportArgs[arg]) 93 | } 94 | yargs.option('input', { 95 | describe: 'Input directory', 96 | type: 'string', 97 | default: '.' 98 | }) 99 | }, 100 | handler: async (argv) => { 101 | const inputDir = argv.input 102 | 103 | const driver = new BotDriver() 104 | const compiler = driver.BuildCompiler() 105 | compiler.ReadScriptsFromDirectory(inputDir) 106 | 107 | const convos = [] 108 | const utterances = Object.keys(compiler.utterances).reduce((acc, u) => acc.concat([compiler.utterances[u]]), []) 109 | 110 | try { 111 | const result = await exportHandler(argv, { convos, utterances }, { statusCallback: (log, obj) => obj ? console.log(log, obj) : console.log(log) }) 112 | console.log(JSON.stringify(result, null, 2)) 113 | } catch (err) { 114 | console.log(`FAILED: ${err.message}`) 115 | } 116 | } 117 | }) 118 | .argv 119 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | const { v1: uuidV1 } = require('uuid') 3 | const mime = require('mime-types') 4 | const dialogflow = require('@google-cloud/dialogflow') 5 | const _ = require('lodash') 6 | const debug = require('debug')('botium-connector-dialogflow') 7 | 8 | const { struct } = require('./structJson') 9 | const { importHandler, importArgs } = require('./src/dialogflowintents') 10 | const { exportHandler, exportArgs } = require('./src/dialogflowintents') 11 | const { extractIntentUtterances, trainIntentUtterances, cleanupIntentUtterances } = require('./src/nlp') 12 | 13 | const Capabilities = { 14 | DIALOGFLOW_PROJECT_ID: 'DIALOGFLOW_PROJECT_ID', 15 | DIALOGFLOW_ENVIRONMENT: 'DIALOGFLOW_ENVIRONMENT', 16 | DIALOGFLOW_CLIENT_EMAIL: 'DIALOGFLOW_CLIENT_EMAIL', 17 | DIALOGFLOW_PRIVATE_KEY: 'DIALOGFLOW_PRIVATE_KEY', 18 | DIALOGFLOW_LANGUAGE_CODE: 'DIALOGFLOW_LANGUAGE_CODE', 19 | DIALOGFLOW_QUERY_PARAMS: 'DIALOGFLOW_QUERY_PARAMS', 20 | DIALOGFLOW_INPUT_CONTEXT_NAME: 'DIALOGFLOW_INPUT_CONTEXT_NAME', 21 | DIALOGFLOW_INPUT_CONTEXT_LIFESPAN: 'DIALOGFLOW_INPUT_CONTEXT_LIFESPAN', 22 | DIALOGFLOW_INPUT_CONTEXT_PARAMETERS: 'DIALOGFLOW_INPUT_CONTEXT_PARAMETERS', 23 | DIALOGFLOW_OUTPUT_PLATFORM: 'DIALOGFLOW_OUTPUT_PLATFORM', 24 | DIALOGFLOW_FORCE_INTENT_RESOLUTION: 'DIALOGFLOW_FORCE_INTENT_RESOLUTION', 25 | DIALOGFLOW_BUTTON_EVENTS: 'DIALOGFLOW_BUTTON_EVENTS', 26 | DIALOGFLOW_ENABLE_KNOWLEDGEBASE: 'DIALOGFLOW_ENABLE_KNOWLEDGEBASE', 27 | DIALOGFLOW_FALLBACK_INTENTS: 'DIALOGFLOW_FALLBACK_INTENTS', 28 | DIALOGFLOW_AUDIOINPUT_ENCODING: 'DIALOGFLOW_AUDIOINPUT_ENCODING', 29 | DIALOGFLOW_AUDIOINPUT_SAMPLERATEHERTZ: 'DIALOGFLOW_AUDIOINPUT_SAMPLERATEHERTZ', 30 | DIALOGFLOW_API_ENDPOINT: 'DIALOGFLOW_API_ENDPOINT' 31 | } 32 | 33 | const Defaults = { 34 | [Capabilities.DIALOGFLOW_LANGUAGE_CODE]: 'en', 35 | [Capabilities.DIALOGFLOW_FORCE_INTENT_RESOLUTION]: true, 36 | [Capabilities.DIALOGFLOW_BUTTON_EVENTS]: true, 37 | [Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE]: false, 38 | [Capabilities.DIALOGFLOW_FALLBACK_INTENTS]: ['Default Fallback Intent'] 39 | } 40 | 41 | class BotiumConnectorDialogflow { 42 | constructor ({ queueBotSays, caps }) { 43 | this.queueBotSays = queueBotSays 44 | this.caps = caps 45 | } 46 | 47 | async Validate () { 48 | debug('Validate called') 49 | this.caps = Object.assign({}, Defaults, this.caps) 50 | 51 | if (!this.caps[Capabilities.DIALOGFLOW_PROJECT_ID]) throw new Error('DIALOGFLOW_PROJECT_ID capability required') 52 | if (!!this.caps[Capabilities.DIALOGFLOW_CLIENT_EMAIL] !== !!this.caps[Capabilities.DIALOGFLOW_PRIVATE_KEY]) throw new Error('DIALOGFLOW_CLIENT_EMAIL and DIALOGFLOW_PRIVATE_KEY capabilities both or none required') 53 | 54 | if (!_.isArray(this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE]) && !_.isBoolean(this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE] && !_.isString(this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE]))) throw new Error('DIALOGFLOW_ENABLE_KNOWLEDGEBASE capability has to be an array of knowledge base identifiers, or a boolean') 55 | if (_.isString(this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE])) { 56 | this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE] = this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE] === 'true' 57 | } 58 | 59 | const contextSuffixes = this._getContextSuffixes() 60 | contextSuffixes.forEach((contextSuffix) => { 61 | if (!this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_NAME + contextSuffix] || !this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_LIFESPAN + contextSuffix]) { 62 | throw new Error(`DIALOGFLOW_INPUT_CONTEXT_NAME${contextSuffix} and DIALOGFLOW_INPUT_CONTEXT_LIFESPAN${contextSuffix} capability required`) 63 | } 64 | }) 65 | } 66 | 67 | async Build () { 68 | debug('Build called') 69 | 70 | this.sessionOpts = { 71 | fallback: true 72 | } 73 | 74 | if (this.caps[Capabilities.DIALOGFLOW_CLIENT_EMAIL] && this.caps[Capabilities.DIALOGFLOW_PRIVATE_KEY]) { 75 | this.sessionOpts.credentials = { 76 | client_email: this.caps[Capabilities.DIALOGFLOW_CLIENT_EMAIL], 77 | private_key: this.caps[Capabilities.DIALOGFLOW_PRIVATE_KEY] 78 | } 79 | } 80 | 81 | if (this.caps[Capabilities.DIALOGFLOW_API_ENDPOINT]) { 82 | this.sessionOpts.apiEndpoint = this.caps[Capabilities.DIALOGFLOW_API_ENDPOINT] 83 | } 84 | } 85 | 86 | async Start () { 87 | debug('Start called') 88 | 89 | this.conversationId = uuidV1() 90 | this.queryParams = {} 91 | 92 | if (this.caps[Capabilities.DIALOGFLOW_QUERY_PARAMS]) { 93 | if (_.isString(this.caps[Capabilities.DIALOGFLOW_QUERY_PARAMS])) { 94 | Object.assign(this.queryParams, JSON.parse(this.caps[Capabilities.DIALOGFLOW_QUERY_PARAMS])) 95 | } else { 96 | Object.assign(this.queryParams, this.caps[Capabilities.DIALOGFLOW_QUERY_PARAMS]) 97 | } 98 | } 99 | if (_.isBoolean(this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE]) && this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE]) { 100 | this.kbClient = new dialogflow.v2beta1.KnowledgeBasesClient(Object.assign({}, this.sessionOpts, { 101 | projectPath: this.caps[Capabilities.DIALOGFLOW_PROJECT_ID] 102 | })) 103 | const formattedParent = this.kbClient.projectPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID]) 104 | const [resources] = await this.kbClient.listKnowledgeBases({ 105 | parent: formattedParent 106 | }) 107 | this.kbNames = resources && resources.map(r => r.name) 108 | } else if (_.isArray(this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE])) { 109 | this.kbNames = this.caps[Capabilities.DIALOGFLOW_ENABLE_KNOWLEDGEBASE] 110 | } 111 | 112 | let useBeta = false 113 | if (this.kbNames && this.kbNames.length > 0) { 114 | debug(`Using Dialogflow Knowledge Bases ${util.inspect(this.kbNames)}, switching to v2beta1 version of Dialogflow API`) 115 | this.queryParams.knowledgeBaseNames = this.kbNames 116 | useBeta = true 117 | } else if (this.caps[Capabilities.DIALOGFLOW_API_ENDPOINT]) { 118 | debug('Using custom api endpoint (for localized dialogflow), switching to v2beta1 version of Dialogflow API') 119 | useBeta = true 120 | } 121 | if (useBeta) { 122 | this.sessionClient = new dialogflow.v2beta1.SessionsClient(this.sessionOpts) 123 | } else { 124 | this.sessionClient = new dialogflow.SessionsClient(this.sessionOpts) 125 | } 126 | 127 | if (this.caps[Capabilities.DIALOGFLOW_ENVIRONMENT]) { 128 | this.sessionPath = this.sessionClient.projectAgentEnvironmentUserSessionPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID], this.caps[Capabilities.DIALOGFLOW_ENVIRONMENT], '-', this.conversationId) 129 | } else { 130 | this.sessionPath = this.sessionClient.projectAgentSessionPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID], this.conversationId) 131 | } 132 | 133 | debug(`Using Dialogflow SessionPath: ${this.sessionPath}`) 134 | this.contextClient = new dialogflow.ContextsClient(this.sessionOpts) 135 | this.queryParams.contexts = this._getContextSuffixes().map((c) => this._createInitialContext(c)) 136 | } 137 | 138 | UserSays (msg) { 139 | debug('UserSays called') 140 | if (!this.sessionClient) return Promise.reject(new Error('not built')) 141 | 142 | const request = { 143 | session: this.sessionPath, 144 | queryInput: { 145 | } 146 | } 147 | if (this.caps[Capabilities.DIALOGFLOW_BUTTON_EVENTS] && msg.buttons && msg.buttons.length > 0 && (msg.buttons[0].text || msg.buttons[0].payload)) { 148 | let payload = msg.buttons[0].payload || msg.buttons[0].text 149 | try { 150 | payload = JSON.parse(payload) 151 | request.queryInput.event = Object.assign({}, { languageCode: this.caps[Capabilities.DIALOGFLOW_LANGUAGE_CODE] }, payload) 152 | if (request.queryInput.event.parameters) { 153 | request.queryInput.event.parameters = struct.encode(request.queryInput.event.parameters) 154 | } 155 | } catch (err) { 156 | request.queryInput.event = { 157 | name: payload, 158 | languageCode: this.caps[Capabilities.DIALOGFLOW_LANGUAGE_CODE] 159 | } 160 | } 161 | } else if (msg.media && msg.media.length > 0) { 162 | const media = msg.media[0] 163 | if (!media.buffer) { 164 | return Promise.reject(new Error(`Media attachment ${media.mediaUri} not downloaded`)) 165 | } 166 | if (!media.mimeType || !media.mimeType.startsWith('audio')) { 167 | return Promise.reject(new Error(`Media attachment ${media.mediaUri} mime type ${media.mimeType || ''} not supported (audio only)`)) 168 | } 169 | request.queryInput.audioConfig = { 170 | audioEncoding: this.caps[Capabilities.DIALOGFLOW_AUDIOINPUT_ENCODING], 171 | sampleRateHertz: this.caps[Capabilities.DIALOGFLOW_AUDIOINPUT_SAMPLERATEHERTZ], 172 | languageCode: this.caps[Capabilities.DIALOGFLOW_LANGUAGE_CODE], 173 | audioChannelCount: this.caps[Capabilities.DIALOGFLOW_AUDIOINPUT_CHANNELS], 174 | enableSeparateRecognitionPerChannel: this.caps[Capabilities.DIALOGFLOW_AUDIOINPUT_RECOGNITION_PER_CHANNEL] 175 | } 176 | request.inputAudio = media.buffer 177 | 178 | if (!msg.attachments) { 179 | msg.attachments = [] 180 | } 181 | msg.attachments.push({ 182 | name: media.mediaUri, 183 | mimeType: media.mimeType, 184 | base64: media.buffer.toString('base64') 185 | }) 186 | } else { 187 | request.queryInput.text = { 188 | text: msg.messageText, 189 | languageCode: this.caps[Capabilities.DIALOGFLOW_LANGUAGE_CODE] 190 | } 191 | } 192 | 193 | const customContexts = this._extractCustomContexts(msg) 194 | // this.queryParams.contexts may contain a value just the first time. 195 | customContexts.forEach(customContext => { 196 | const index = this.queryParams.contexts.findIndex(c => c.name === customContext.name) 197 | if (index >= 0) { 198 | this.queryParams.contexts[index] = customContext 199 | } else { 200 | this.queryParams.contexts.push(customContext) 201 | } 202 | }) 203 | 204 | const mergeQueryParams = {} 205 | if (msg.SET_DIALOGFLOW_QUERYPARAMS) { 206 | Object.assign(mergeQueryParams, msg.SET_DIALOGFLOW_QUERYPARAMS) 207 | } 208 | 209 | request.queryParams = Object.assign({}, this.queryParams, mergeQueryParams) 210 | if (request.queryParams.payload) { 211 | request.queryParams.payload = struct.encode(request.queryParams.payload) 212 | } 213 | 214 | debug(`dialogflow request: ${JSON.stringify(_.omit(request, ['inputAudio']), null, 2)}`) 215 | msg.sourceData = request 216 | 217 | return this.sessionClient.detectIntent(request) 218 | .then((responses) => { 219 | this.queryParams.contexts = [] 220 | const response = responses[0] 221 | 222 | debug(`dialogflow response: ${JSON.stringify(_.omit(response, ['outputAudio']), null, 2)}`) 223 | let decoded = false 224 | if (response.queryResult.parameters) { 225 | response.queryResult.parameters = struct.decode(response.queryResult.parameters) 226 | } 227 | if (response.queryResult.outputContexts) { 228 | response.queryResult.outputContexts.forEach(context => { 229 | if (context.parameters) { 230 | context.parameters = struct.decode(context.parameters) 231 | decoded = true 232 | } 233 | }) 234 | } 235 | if (decoded) debug(`dialogflow response (after struct.decode): ${JSON.stringify(_.omit(response, ['outputAudio']), null, 2)}`) 236 | 237 | const nlp = { 238 | intent: this._extractIntent(response), 239 | entities: this._extractEntities(response) 240 | } 241 | const audioAttachment = this._getAudioOutput(response) 242 | const attachments = audioAttachment ? [audioAttachment] : [] 243 | 244 | const outputPlatform = this.caps[Capabilities.DIALOGFLOW_OUTPUT_PLATFORM] 245 | const ffSrc = response.queryResult.fulfillmentMessages ? JSON.parse(JSON.stringify(response.queryResult.fulfillmentMessages)) : [] 246 | let fulfillmentMessages = ffSrc.filter(f => { 247 | if (outputPlatform && f.platform === outputPlatform) { 248 | return true 249 | } else if (!outputPlatform && (f.platform === 'PLATFORM_UNSPECIFIED' || !f.platform)) { 250 | return true 251 | } 252 | return false 253 | }) 254 | 255 | // use default if platform specific is not found 256 | if (fulfillmentMessages.length === 0 && outputPlatform) { 257 | fulfillmentMessages = ffSrc.filter(f => 258 | (f.platform === 'PLATFORM_UNSPECIFIED' || !f.platform)) 259 | } 260 | 261 | let forceIntentResolution = this.caps[Capabilities.DIALOGFLOW_FORCE_INTENT_RESOLUTION] 262 | fulfillmentMessages.forEach((fulfillmentMessage) => { 263 | let acceptedResponse = true 264 | const botMsg = { sender: 'bot', sourceData: response.queryResult, nlp, attachments } 265 | if (fulfillmentMessage.text) { 266 | botMsg.messageText = fulfillmentMessage.text.text[0] 267 | } else if (fulfillmentMessage.simpleResponses) { 268 | botMsg.messageText = fulfillmentMessage.simpleResponses.simpleResponses[0].textToSpeech 269 | } else if (fulfillmentMessage.image) { 270 | botMsg.media = [{ 271 | mediaUri: fulfillmentMessage.image.imageUri, 272 | mimeType: mime.lookup(fulfillmentMessage.image.imageUri) || 'application/unknown' 273 | }] 274 | } else if (fulfillmentMessage.quickReplies) { 275 | botMsg.messageText = fulfillmentMessage.quickReplies.title 276 | botMsg.buttons = fulfillmentMessage.quickReplies.quickReplies.map((q) => ({ text: q })) 277 | } else if (fulfillmentMessage.card) { 278 | botMsg.messageText = fulfillmentMessage.card.title 279 | botMsg.cards = [{ 280 | text: fulfillmentMessage.card.title, 281 | image: fulfillmentMessage.card.imageUri && { 282 | mediaUri: fulfillmentMessage.card.imageUri, 283 | mimeType: mime.lookup(fulfillmentMessage.card.imageUri) || 'application/unknown' 284 | }, 285 | buttons: fulfillmentMessage.card.buttons && fulfillmentMessage.card.buttons.map((q) => ({ text: q.text, payload: q.postback })) 286 | }] 287 | } else if (fulfillmentMessage.basicCard) { 288 | botMsg.messageText = fulfillmentMessage.basicCard.title 289 | botMsg.cards = [{ 290 | text: fulfillmentMessage.basicCard.title, 291 | image: fulfillmentMessage.basicCard.image && { 292 | mediaUri: fulfillmentMessage.basicCard.image.imageUri, 293 | mimeType: mime.lookup(fulfillmentMessage.basicCard.image.imageUri) || 'application/unknown', 294 | altText: fulfillmentMessage.basicCard.image.accessibilityText 295 | }, 296 | buttons: fulfillmentMessage.basicCard.buttons && fulfillmentMessage.basicCard.buttons.map((q) => ({ text: q.title, payload: q.openUriAction && q.openUriAction.uri })) 297 | }] 298 | } else if (fulfillmentMessage.listSelect) { 299 | botMsg.messageText = fulfillmentMessage.listSelect.title 300 | botMsg.cards = fulfillmentMessage.listSelect.items.map(item => ({ 301 | text: item.title, 302 | subtext: item.description, 303 | image: item.image && { 304 | mediaUri: item.image.imageUri, 305 | mimeType: mime.lookup(item.image.imageUri) || 'application/unknown', 306 | altText: item.image.accessibilityText 307 | }, 308 | buttons: item.info && item.info.key && [{ text: item.info.key }] 309 | })) 310 | } else if (fulfillmentMessage.carouselSelect) { 311 | botMsg.cards = fulfillmentMessage.carouselSelect.items.map(item => ({ 312 | text: item.title, 313 | subtext: item.description, 314 | image: item.image && { 315 | mediaUri: item.image.imageUri, 316 | mimeType: mime.lookup(item.image.imageUri) || 'application/unknown', 317 | altText: item.image.accessibilityText 318 | }, 319 | buttons: item.info && item.info.key && [{ text: item.info.key }] 320 | })) 321 | } else if (fulfillmentMessage.suggestions) { 322 | botMsg.buttons = fulfillmentMessage.suggestions.suggestions && fulfillmentMessage.suggestions.suggestions.map((q) => ({ text: q.title })) 323 | } else if (fulfillmentMessage.linkOutSuggestion) { 324 | botMsg.buttons = [{ text: fulfillmentMessage.linkOutSuggestion.destinationName, payload: fulfillmentMessage.linkOutSuggestion.uri }] 325 | } else { 326 | acceptedResponse = false 327 | } 328 | 329 | if (acceptedResponse) { 330 | setTimeout(() => this.queueBotSays(botMsg), 0) 331 | forceIntentResolution = false 332 | } 333 | }) 334 | 335 | if (forceIntentResolution) { 336 | setTimeout(() => this.queueBotSays({ sender: 'bot', sourceData: response.queryResult, nlp, attachments }), 0) 337 | } 338 | }).catch((err) => { 339 | debug(err) 340 | throw new Error(`Cannot send message to dialogflow container: ${err.message}`) 341 | }) 342 | } 343 | 344 | async Stop () { 345 | debug('Stop called') 346 | this.sessionClient = null 347 | this.sessionPath = null 348 | this.queryParams = null 349 | } 350 | 351 | async Clean () { 352 | debug('Clean called') 353 | this.sessionOpts = null 354 | } 355 | 356 | _getAudioOutput (response) { 357 | if (response.outputAudio && response.outputAudioConfig) { 358 | const acSrc = JSON.parse(JSON.stringify(response.outputAudioConfig)) 359 | const attachment = { 360 | } 361 | if (acSrc.audioEncoding === 'OUTPUT_AUDIO_ENCODING_LINEAR_16') { 362 | attachment.name = 'output.wav' 363 | attachment.mimeType = 'audio/wav' 364 | } else if (acSrc.audioEncoding === 'OUTPUT_AUDIO_ENCODING_MP3') { 365 | attachment.name = 'output.mp3' 366 | attachment.mimeType = 'audio/mpeg3' 367 | } else if (acSrc.audioEncoding === 'OUTPUT_AUDIO_ENCODING_OGG_OPUS') { 368 | attachment.name = 'output.ogg' 369 | attachment.mimeType = 'audio/ogg' 370 | } 371 | if (attachment.name) { 372 | attachment.base64 = Buffer.from(response.outputAudio).toString('base64') 373 | return attachment 374 | } 375 | } 376 | } 377 | 378 | _createInitialContext (contextSuffix) { 379 | let contextPath = null 380 | if (this.caps[Capabilities.DIALOGFLOW_ENVIRONMENT]) { 381 | contextPath = this.contextClient.projectAgentEnvironmentUserSessionContextPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID], this.caps[Capabilities.DIALOGFLOW_ENVIRONMENT], '-', this.conversationId, this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_NAME + contextSuffix]) 382 | } else { 383 | contextPath = this.contextClient.projectAgentSessionContextPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID], this.conversationId, this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_NAME + contextSuffix]) 384 | } 385 | 386 | return { 387 | name: contextPath, 388 | lifespanCount: parseInt(this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_LIFESPAN + contextSuffix]), 389 | parameters: this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_PARAMETERS + contextSuffix] && 390 | struct.encode(this.caps[Capabilities.DIALOGFLOW_INPUT_CONTEXT_PARAMETERS + contextSuffix]) 391 | } 392 | } 393 | 394 | _getContextSuffixes () { 395 | const suffixes = [] 396 | const contextNameCaps = _.pickBy(this.caps, (v, k) => k.startsWith(Capabilities.DIALOGFLOW_INPUT_CONTEXT_NAME)) 397 | _(contextNameCaps).keys().sort().each((key) => { 398 | suffixes.push(key.substring(Capabilities.DIALOGFLOW_INPUT_CONTEXT_NAME.length)) 399 | }) 400 | return suffixes 401 | } 402 | 403 | _extractCustomContexts (msg) { 404 | const result = [] 405 | if (msg.SET_DIALOGFLOW_CONTEXT) { 406 | _.keys(msg.SET_DIALOGFLOW_CONTEXT).forEach(contextName => { 407 | const val = msg.SET_DIALOGFLOW_CONTEXT[contextName] 408 | if (_.isObject(val)) { 409 | result.push(this._createCustomContext(contextName, val.lifespan, val.parameters)) 410 | } else { 411 | result.push(this._createCustomContext(contextName, val)) 412 | } 413 | }) 414 | } 415 | return result 416 | } 417 | 418 | _createCustomContext (contextName, contextLifespan, contextParameters) { 419 | let contextPath = null 420 | if (this.caps[Capabilities.DIALOGFLOW_ENVIRONMENT]) { 421 | contextPath = this.contextClient.projectAgentEnvironmentUserSessionContextPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID], this.caps[Capabilities.DIALOGFLOW_ENVIRONMENT], '-', this.conversationId, contextName) 422 | } else { 423 | contextPath = this.contextClient.projectAgentSessionContextPath(this.caps[Capabilities.DIALOGFLOW_PROJECT_ID], this.conversationId, contextName) 424 | } 425 | try { 426 | contextLifespan = parseInt(contextLifespan) 427 | } catch (err) { 428 | contextLifespan = 1 429 | } 430 | 431 | const context = { 432 | name: contextPath, 433 | lifespanCount: contextLifespan 434 | } 435 | if (contextParameters) { 436 | context.parameters = struct.encode(contextParameters) 437 | } 438 | return context 439 | } 440 | 441 | _extractIntent (response) { 442 | if (response.queryResult.intent) { 443 | return { 444 | name: response.queryResult.intent.displayName, 445 | confidence: response.queryResult.intentDetectionConfidence, 446 | incomprehension: this.caps.DIALOGFLOW_FALLBACK_INTENTS.includes(response.queryResult.intent.displayName) ? true : undefined 447 | } 448 | } 449 | return {} 450 | } 451 | 452 | _extractEntities (response) { 453 | if (response.queryResult.parameters && Object.keys(response.queryResult.parameters).length > 0) { 454 | return this._extractEntitiesFromFields('', response.queryResult.parameters) 455 | } 456 | return [] 457 | } 458 | 459 | _extractEntitiesFromFields (keyPrefix, fields) { 460 | return Object.keys(fields).reduce((entities, key) => { 461 | return entities.concat(this._extractEntityValues(`${keyPrefix ? keyPrefix + '.' : ''}${key}`, fields[key])) 462 | }, []) 463 | } 464 | 465 | _extractEntityValues (key, field) { 466 | if (_.isNull(field) || _.isUndefined(field)) { 467 | return [] 468 | } else if (_.isString(field) || _.isNumber(field) || _.isBoolean(field)) { 469 | return [{ 470 | name: key, 471 | value: field 472 | }] 473 | } else if (_.isArray(field)) { 474 | return field.reduce((entities, lv, i) => { 475 | return entities.concat(this._extractEntityValues(`${key}.${i}`, lv)) 476 | }, []) 477 | } else if (_.isObject(field)) { 478 | return this._extractEntitiesFromFields(key, field) 479 | } 480 | debug(`Unsupported entity kind for ${key}, skipping entity.`) 481 | return [] 482 | } 483 | } 484 | 485 | const audioEncodingList = [ 486 | 'AUDIO_ENCODING_UNSPECIFIED', 487 | 'AUDIO_ENCODING_LINEAR_16', 488 | 'AUDIO_ENCODING_FLAC', 489 | 'AUDIO_ENCODING_MULAW', 490 | 'AUDIO_ENCODING_AMR', 491 | 'AUDIO_ENCODING_AMR_WB', 492 | 'AUDIO_ENCODING_OGG_OPUS', 493 | 'AUDIO_ENCODING_SPEEX_WITH_HEADER_BYTE' 494 | ] 495 | 496 | module.exports = { 497 | PluginVersion: 1, 498 | PluginClass: BotiumConnectorDialogflow, 499 | Import: { 500 | Handler: importHandler, 501 | Args: importArgs 502 | }, 503 | Export: { 504 | Handler: exportHandler, 505 | Args: exportArgs 506 | }, 507 | NLP: { 508 | ExtractIntentUtterances: extractIntentUtterances, 509 | TrainIntentUtterances: trainIntentUtterances, 510 | CleanupIntentUtterances: cleanupIntentUtterances 511 | }, 512 | PluginDesc: { 513 | name: 'Google Dialogflow ES', 514 | provider: 'Google', 515 | features: { 516 | intentResolution: true, 517 | intentConfidenceScore: true, 518 | entityResolution: true, 519 | testCaseGeneration: true, 520 | testCaseExport: true, 521 | audioInput: true, 522 | supportedFileExtensions: ['.wav', '.pcm', '.m4a', '.flac', '.riff', '.wma', '.aac', '.ogg', '.oga', '.mp3', '.amr'] 523 | }, 524 | capabilities: [ 525 | { 526 | name: 'DIALOGFLOW_API_ENDPOINT', 527 | label: 'Dialogflow Region', 528 | description: 'For more information on what region to use, consult the Dialogflow Documentation', 529 | type: 'choice', 530 | required: false, 531 | advanced: true, 532 | choices: [ 533 | { name: 'Europe/Belgium', key: 'europe-west1-dialogflow.googleapis.com' }, 534 | { name: 'Europe/London', key: 'europe-west2-dialogflow.googleapis.com' }, 535 | { name: 'Asia Pacific/Sydney', key: 'australia-southeast1-dialogflow.googleapis.com' }, 536 | { name: 'Asia Pacific/Tokyo', key: 'asia-northeast1-dialogflow.googleapis.com' }, 537 | { name: 'Global', key: 'global-dialogflow.googleapis.com' } 538 | ] 539 | }, 540 | { 541 | name: 'DIALOGFLOW_LANGUAGE_CODE', 542 | label: 'Language', 543 | description: 'For more information about supported languages, consult the Dialogflow Documentation', 544 | type: 'query', 545 | required: true, 546 | advanced: true, 547 | query: async (caps) => { 548 | if (caps && caps.DIALOGFLOW_CLIENT_EMAIL && caps.DIALOGFLOW_PRIVATE_KEY && caps.DIALOGFLOW_PROJECT_ID) { 549 | try { 550 | const sessionOpts = { 551 | credentials: { 552 | client_email: caps[Capabilities.DIALOGFLOW_CLIENT_EMAIL], 553 | private_key: caps[Capabilities.DIALOGFLOW_PRIVATE_KEY] 554 | } 555 | } 556 | if (caps.DIALOGFLOW_API_ENDPOINT) { 557 | sessionOpts.apiEndpoint = caps.DIALOGFLOW_API_ENDPOINT 558 | } 559 | const agentsClient = new dialogflow.v2beta1.AgentsClient(sessionOpts) 560 | const projectPath = agentsClient.projectPath(caps.DIALOGFLOW_PROJECT_ID) 561 | const allResponses = await agentsClient.getAgent({ parent: projectPath }) 562 | if (allResponses && allResponses.length > 0) { 563 | return _.uniq([allResponses[0].defaultLanguageCode, ...allResponses[0].supportedLanguageCodes]).map(l => ({ name: l, key: l })) 564 | } 565 | } catch (err) { 566 | throw new Error(`Dialogflow Agent Query failed: ${err.message}`) 567 | } 568 | } 569 | } 570 | }, 571 | { 572 | name: 'DIALOGFLOW_OUTPUT_PLATFORM', 573 | label: 'Output Platform', 574 | description: 'Find out more about integrations in the Dialogflow Documentation', 575 | type: 'choice', 576 | required: true, 577 | advanced: true, 578 | choices: [ 579 | { name: 'Default platform', key: 'PLATFORM_UNSPECIFIED' }, 580 | { name: 'Facebook', key: 'FACEBOOK' }, 581 | { name: 'Slack', key: 'SLACK' }, 582 | { name: 'Telegram', key: 'TELEGRAM' }, 583 | { name: 'Kik', key: 'KIK' }, 584 | { name: 'Skype', key: 'SKYPE' }, 585 | { name: 'Line', key: 'LINE' }, 586 | { name: 'Viber', key: 'VIBER' }, 587 | { name: 'Google Assistant', key: 'ACTIONS_ON_GOOGLE' }, 588 | { name: 'Google Hangouts', key: 'GOOGLE_HANGOUTS' }, 589 | { name: 'Telephony', key: 'TELEPHONY' } 590 | ] 591 | }, 592 | { 593 | name: 'DIALOGFLOW_AUDIOINPUT_ENCODING', 594 | label: 'Audio Input Encoding', 595 | description: 'Details about audio encodings are available in the Dialogflow Documentation', 596 | type: 'choice', 597 | required: true, 598 | advanced: true, 599 | choices: audioEncodingList.map(l => ({ name: l, key: l })) 600 | } 601 | ], 602 | actions: [ 603 | { 604 | name: 'GetAgentMetaData', 605 | description: 'GetAgentMetaData', 606 | run: async (caps) => { 607 | if (caps && caps.DIALOGFLOW_CLIENT_EMAIL && caps.DIALOGFLOW_PRIVATE_KEY && caps.DIALOGFLOW_PROJECT_ID) { 608 | try { 609 | const sessionOpts = { 610 | credentials: { 611 | client_email: caps[Capabilities.DIALOGFLOW_CLIENT_EMAIL], 612 | private_key: caps[Capabilities.DIALOGFLOW_PRIVATE_KEY] 613 | } 614 | } 615 | if (caps.DIALOGFLOW_API_ENDPOINT) { 616 | sessionOpts.apiEndpoint = caps.DIALOGFLOW_API_ENDPOINT 617 | } 618 | const agentsClient = new dialogflow.v2beta1.AgentsClient(sessionOpts) 619 | const projectPath = agentsClient.projectPath(caps.DIALOGFLOW_PROJECT_ID) 620 | 621 | const agentResponses = await agentsClient.getAgent({ parent: projectPath }) 622 | const agentInfo = agentResponses[0] 623 | 624 | return { 625 | name: agentInfo.displayName, 626 | description: agentInfo.description, 627 | metadata: agentInfo 628 | } 629 | } catch (err) { 630 | throw new Error(`Dialogflow Agent Query failed: ${err.message}`) 631 | } 632 | } 633 | } 634 | } 635 | ] 636 | } 637 | } 638 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botium-connector-dialogflow", 3 | "version": "0.1.3", 4 | "description": "Botium Connector for Google Dialogflow", 5 | "main": "dist/botium-connector-dialogflow-cjs.js", 6 | "module": "dist/botium-connector-dialogflow-es.js", 7 | "engines": { 8 | "node": ">=10" 9 | }, 10 | "bin": { 11 | "botium-connector-dialogflow-cli": "./bin/cli.js" 12 | }, 13 | "scripts": { 14 | "build": "npm run eslint && rollup -c", 15 | "eslint": "eslint index.js src/**.js bin/**.js", 16 | "eslint-fix": "eslint --fix index.js src/**.js bin/**.js", 17 | "test": "echo \"Placeholder\"" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/codeforequity-at/botium-connector-dialogflow.git" 22 | }, 23 | "author": "Botium GmbH", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/codeforequity-at/botium-core/issues" 27 | }, 28 | "homepage": "https://www.botium.ai", 29 | "devDependencies": { 30 | "@babel/core": "^7.9.6", 31 | "@babel/node": "^7.8.7", 32 | "@babel/plugin-transform-runtime": "^7.9.6", 33 | "@babel/preset-env": "^7.9.6", 34 | "eslint": "^6.8.0", 35 | "eslint-config-standard": "^14.1.1", 36 | "eslint-plugin-import": "^2.20.2", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.0.1", 40 | "rollup": "^2.7.6", 41 | "rollup-plugin-babel": "^4.4.0", 42 | "rollup-plugin-commonjs": "^10.1.0", 43 | "rollup-plugin-json": "^4.0.0", 44 | "rollup-plugin-node-resolve": "^5.2.0" 45 | }, 46 | "dependencies": { 47 | "@babel/runtime": "^7.9.6", 48 | "@google-cloud/dialogflow": "^6.8.0", 49 | "debug": "^4.3.7", 50 | "jszip": "^3.10.1", 51 | "lodash": "^4.17.21", 52 | "mime-types": "^2.1.35", 53 | "mkdirp": "^3.0.1", 54 | "randomatic": "^3.1.1", 55 | "slug": "^9.1.0", 56 | "uuid": "^10.0.0", 57 | "yargs": "^17.7.2" 58 | }, 59 | "peerDependencies": { 60 | "botium-core": ">= 1.14.9" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import json from 'rollup-plugin-json' 4 | 5 | export default { 6 | input: 'index.js', 7 | output: [ 8 | { 9 | file: 'dist/botium-connector-dialogflow-es.js', 10 | format: 'es', 11 | sourcemap: true 12 | }, 13 | { 14 | file: 'dist/botium-connector-dialogflow-cjs.js', 15 | format: 'cjs', 16 | sourcemap: true 17 | } 18 | ], 19 | plugins: [ 20 | commonjs({ 21 | exclude: 'node_modules/**' 22 | }), 23 | json(), 24 | babel({ 25 | exclude: 'node_modules/**', 26 | runtimeHelpers: true 27 | }) 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /samples/CaffeShop/botium.json: -------------------------------------------------------------------------------- 1 | { 2 | "botium": { 3 | "Capabilities": { 4 | "PROJECTNAME": "Google Dialogflow Room Reservation Sample", 5 | "CONTAINERMODE": "dialogflow", 6 | "DIALOGFLOW_PROJECT_ID": "bd-gcp-273718", 7 | "DIALOGFLOW_ENVIRONMENT": "DEV", 8 | "DIALOGFLOW_CLIENT_EMAIL": "botiumtesting@bd-gcp-273718.iam.gserviceaccount.com", 9 | "DIALOGFLOW_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDAEoxJxYtZn/eJ\n24ZZ2uTehLe4W67/1RTEdHDST1hsOgw2t7Eo1VmENGz3vdxn/MZBzwH4Zwq2oeC8\nXDXtJCZ3rRAL88ic9Jcm9UQfPNYxESkLl4/6ksjeOTdzGPyS1tTHDD06doEtjOkt\nogEsUM6ow2QCT3GR2YnRAGcJszK93zPSSrUgguLuHNpFPHG5GesBsjc2vwbF1QoH\nlifXgVUBh5GUedbXmxmWyUS3J3KRCqAlsR6W7C0ai/f16DqOf3YwgTIOj5Qa0TQZ\nDM/xZ+Y1ItBE9IkX4pa6/Pcfk2XNhkDbgXrNX8O9Ns/Eluse8p+YLxL0C5K1OyS3\nGBgJnj/tAgMBAAECggEAFYR+bu1QlTPUmX8caAibx0n7Aw5SZprKfs/Eq+rzrvw5\nzg96K2brZmIxKvf8LzIkS88TG27/xkIQWUnDSNPZ82JR/+TJvruwC09C2HUKUejo\nPkI2gQ3crvid+6j68sBaigoW+eu4wQ+eN9yEYSCUZletZozb3kkOpXl0EaQftSGQ\nfRPoEdxLDbTJikUTw3TdF1NtdxRDvK3JCZnSY3r5ENNx8VQCW20YsCaxXwIbzvv6\nVZ79o1Ka3i8Dmft1UBfsq8EdHwZd2l+aoNkiqHT3PDz+/shzE1QuhkegS7Xd68Mu\nY9IT+Mg6b2FNKXKFOynQuHbcP/KmKVZlOeZRmpO0gQKBgQDqBNyNOkiJxn2+ivzN\nKJYF1D0nEsT36iFDVNrSqpVey/uOz+3/YTGtJHcDkm82bmjfq4cQlHJz7iwcluTK\nIYIFvYzxzrMVXBx6DQHjxiYvYituvIPZnaTREMa/rLOdEIcP03Db4YHfuVBfC+pN\nA/C78RNZxMnazU8PnTysy+PJnQKBgQDSHQ4FG/tGdzIRXqn9HD5rV7L1+NHcnZWX\nKOdn9W8veCBzksGwSm123QniqXHpq2QhE0H/+1qdUj69a3WNLCmOKuywYTZ+TPxh\nU88bQTyQf0Z88mNe8L+ONXgG745YegMXZ07DOUQ4aL9MtizEri7fdXiWJijjfcPT\nQplByW7mkQKBgHBF2NJgzgBbnGRYJiT/fcpaIuSmGp2eVEsLYNiFQphyYMQJefN1\n2/FndzndfUGuAt6cWnZX4flAoSUuN6HKJak/YWl5c22h/X+I0glHPXaqMJA2HUPA\nYBx1YgT/hESscz/if1jefgKp06dl/gjpBQwGAvSkdvtkWLPzCfMU9rn5AoGAZtE8\n+RcWQQc3AvT17hwSF2kU8/TMjU630v57Lo4V2H7KMoQQL/pb0pybp8UPLy9lkiHg\nXCld0Dpc5Uhef7SqqR0sMauAhRGbNu1SHZ4wyuzdS2s5YU4iDq7vi0VSvM0IEj3F\ndxbg4VGwuxMymRQ7b7IGTkNiIuB6ITkn0d+GW2ECgYAt4unX5HMVswVwOWCTf+du\nlDfnARU3F+yveI+DIWnPo5RKGkIMIvYaUENdFvpfi1KGGRTzNEJPQJ5dFFnvA4Ik\ng3kEJImEtuRzMZN/lTWdwkrDRBV9JtyBsNeHbyj5zBpV4KXQ4PXdWOINXF+Gxex0\nnbDxUX4cOG/PaKPE2N9szQ==\n-----END PRIVATE KEY-----\n", 10 | "DIALOGFLOW_LANGUAGE_CODE": "en", 11 | "DIALOGFLOW_OUTPUT_PLATFORM": "", 12 | "DIALOGFLOW_BUTTON_EVENTS": true, 13 | "DIALOGFLOW_AUDIOINPUT_ENCODING": "", 14 | "DIALOGFLOW_AUDIOINPUT_SAMPLERATEHERTZ": null, 15 | "DIALOGFLOW_AUDIOINPUT_CHANNELS": null, 16 | "DIALOGFLOW_AUDIOINPUT_RECOGNITION_PER_CHANNEL": false, 17 | "DIALOGFLOW_API_ENDPOINT": "" 18 | }, 19 | "Sources": {}, 20 | "Envs": {} 21 | } 22 | } -------------------------------------------------------------------------------- /samples/CaffeShop/botium.local1.json: -------------------------------------------------------------------------------- 1 | { 2 | "botium": { 3 | "Capabilities": { 4 | "DIALOGFLOW_CLIENT_EMAIL": "banking-2-b32c1@appspot.gserviceaccount.com", 5 | "DIALOGFLOW_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZVqgfGQL4Ygft\n06zrNI7435efQc+CjwvRadQXlLX7xrrmeVqCo6B31kz5gO6W66VEh1mW/DW+Aj4j\nIrTyOYPoiwUxSTIlY5vyBIDTuKk1cvUFyUASbJpEAzJ1BjHc9G+kZAyk0Tn6G2gq\nHwgaXQq9sNPhV7QJnERPvyhm05P+Vcx9KwPGCJwtiGXBZ9JEAwRx9WVatHIijyAH\nUalH8FbIPFUh2xi8igyTV01qM3hpeT31wIckZjtOkeeVsOaXoPAVolDZSgy8cson\nLbYfNcroijO7Nn8cA0haR7oeZoNctttJxaJo91Qkqpjk4TkpKb7PgyjKj1rWkBW5\nfuI8rEZJAgMBAAECggEABJKJMl4yULpJnr2mZSAc2E6AjLLvb2b5FP5JIQfh8ldP\nePJq8MTU0uX8xx7hXnY56CR6Fy6/ooIaHL2a9DMjXBKTLdBPMr645ntcZRPXVdzu\n8gR5iOMcqYUH4uXzpDFUx+vk1aBYkHzzOvhQa0rVeGkWtFxDZdr3TYJ3f7N08QtN\nm36OH9zFQ6vDkEZuz3vpqLyAwC4wi4NXHd08o4vMtWxj2HAP/QVOwebq6dJRVEFF\nBewed86gch3bxCK8AZKXgqVDAD/5JyBbSZ+UsOGqX1hcE9wFbuPAGVNZ9ZjY0imW\n4iv/t9hUMOnwTS2e3cDCjdmJAAKITgANUS0WQL3MpQKBgQDJ7MAKGFbaFmm0eY13\nIiwbXs5Q0WlFUQ2ihvS9dytCB/v1R/8byB2ReuuFEVVtuSRbtfahkvpRbew/XOrE\nRlrqcfbcRL472XecLVMazYomeHYM0j5MMCidl5P8wg7oJ+rViv8hmxSVGJHPiFiS\nFglpOfGifyW9MXJ/ZlBJoth8bQKBgQDCZwBtQtCfvn89LgwO1mv3PFRwgS3/sj7U\nKV4h6arQzNBvdlBjYhg2sA8xNhcakAcqmYCvHax2NVystYxQMQkZB0or18iUDvZH\nSy/xSlp9TRrgLQrDGDzqevzt7zb3VA0ninUrL4mZMLGAFhnCG/wbNf6qFV9KlsHp\n3u9mSbdPzQKBgCX1e6RqLumQjQ6ASze6FNCQYfiPZVrCge2rsRZs8JTZK2RQxIRU\nejp/AQdi2sxloZAcBOEa4DOullrS/M8o6q5n/iAqTBi0KOgTHSxMt0vEGW0PmOZ5\npyj49bjuQh8iYeMC3jNTE+tzUvXQi9XqjppZajNB87Y9NWj9stok4s2BAoGAVV6+\nRT0OONiDL+7ExM9M4mnP+wf/l0ZDaRAxklr29HK9JfWjH8G4OGvggA/s3XFadE6s\nwDobvl14sS+Yyq59/EvDOrWsE7juViiiiSfJzTTLFJ8zFrHSLnaMFED0tnBmxnJ7\nOdfKaUeE62bJMQDxGVIA39g63JqP1ZBdOWIt6hECgYB7ES6tG7xrDr0cdys7ebMw\n5Sqtq8Xv6PIYk0xncGw6ZP77beyt3ZRH/BZQ1jbUWyTqxEusjm/v4So4k4Dc8oQ6\nOYWycZ8GSJQASeOP2X3pizXSBOq8+LSTc0JDv/Nuf2MM/ILMWXAsspSQ6xQAomQJ\nypHuX+g2C6RD7RojteKMEg==\n-----END PRIVATE KEY-----\n" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/CaffeShop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "botiumFluent.js", 6 | "scripts": { 7 | "test": "mocha spec" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "botium": { 13 | "convodirs": [ 14 | "spec/convo" 15 | ], 16 | "expandConvos": true, 17 | "expandUtterancesToConvos": false 18 | }, 19 | "devDependencies": { 20 | "botium-bindings": "latest", 21 | "botium-connector-dialogflow": "../..", 22 | "cross-env": "^5.2.0", 23 | "mocha": "latest" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/CaffeShop/spec/botium.spec.js: -------------------------------------------------------------------------------- 1 | const bb = require('botium-bindings') 2 | bb.helper.mocha().setupMochaTestSuite() 3 | -------------------------------------------------------------------------------- /samples/CaffeShop/spec/convo/follow_up_intent.convo.txt: -------------------------------------------------------------------------------- 1 | follow up intent 2 | 3 | #me 4 | Hello 5 | 6 | #bot 7 | 8 | -------------------------------------------------------------------------------- /samples/RoomReservation/botium.json: -------------------------------------------------------------------------------- 1 | { 2 | "botium": { 3 | "Capabilities": { 4 | "PROJECTNAME": "Google Dialogflow Room Reservation Sample", 5 | "CONTAINERMODE": "dialogflow", 6 | "DIALOGFLOW_PROJECT_ID": "room-reservation-cfa4c", 7 | "DIALOGFLOW_ENVIRONMENT": "TEST", 8 | "DIALOGFLOW_BUTTON_EVENTS": true, 9 | "DIALOGFLOW_OUTPUT_PLATFORM": "FACEBOOK", 10 | "DIALOGFLOW_INPUT_CONTEXT_NAME": "testsession", 11 | "DIALOGFLOW_INPUT_CONTEXT_LIFESPAN": 5, 12 | "DIALOGFLOW_INPUT_CONTEXT_NAME_0": "testbotium", 13 | "DIALOGFLOW_INPUT_CONTEXT_LIFESPAN_0": 5, 14 | "DIALOGFLOW_INPUT_CONTEXT_PARAMETERS_0": { 15 | "IsBotiumTest": true 16 | }, 17 | "USER_INPUTS": [ 18 | { 19 | "ref": "MEDIA", 20 | "src": "MediaInput", 21 | "args": { 22 | "downloadMedia": true 23 | } 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/RoomReservation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "mocha spec" 6 | }, 7 | "botium": { 8 | "convodirs": [ 9 | "spec/convo" 10 | ], 11 | "expandConvos": true, 12 | "expandUtterancesToConvos": false 13 | }, 14 | "devDependencies": { 15 | "botium-bindings": "latest", 16 | "botium-connector-dialogflow": "../..", 17 | "cross-env": "latest", 18 | "mocha": "latest" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/RoomReservation/spec/botium.spec.js: -------------------------------------------------------------------------------- 1 | const bb = require('botium-bindings') 2 | bb.helper.mocha().setupMochaTestSuite() 3 | -------------------------------------------------------------------------------- /samples/RoomReservation/spec/convo/audio.convo.txt: -------------------------------------------------------------------------------- 1 | audio 2 | 3 | #me 4 | MEDIA bookroom.wav 5 | 6 | #bot 7 | BUTTONS Let's book a room. 8 | -------------------------------------------------------------------------------- /samples/RoomReservation/spec/convo/bookroom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforequity-at/botium-connector-dialogflow/02e49160a8dc971232415b5c0a1df92ca8199797/samples/RoomReservation/spec/convo/bookroom.wav -------------------------------------------------------------------------------- /samples/RoomReservation/spec/convo/buttons.convo.txt: -------------------------------------------------------------------------------- 1 | buttons 2 | 3 | #me 4 | buttons 5 | 6 | #bot 7 | BUTTONS QuickReply1|QuickReply2|QuickReply3 8 | 9 | #me 10 | BUTTON { "name": "QuickReply1", "parameters": { "param1": "value1" } } 11 | 12 | #bot 13 | You selected QuickReply1 -------------------------------------------------------------------------------- /samples/RoomReservation/spec/convo/context.convo.txt: -------------------------------------------------------------------------------- 1 | context 2 | 3 | #me 4 | hi 5 | UPDATE_CUSTOM SET_DIALOGFLOW_CONTEXT|mycontext1|7 6 | UPDATE_CUSTOM SET_DIALOGFLOW_CONTEXT|mycontext2|{"lifespan": 4, "parameters": { "test": "test1"}} 7 | 8 | #bot 9 | Enter "card" or "picture" or "buttons" or "adaptive" for rich message content 10 | JSON_PATH $.outputContexts[0].name|testsession 11 | JSON_PATH $.outputContexts[1].name|testbotium 12 | JSON_PATH $.outputContexts[2].name|mycontext1 13 | JSON_PATH $.outputContexts[3].name|mycontext2 14 | JSON_PATH $.outputContexts[?(@.name.indexOf('testbotium') >= 0)].parameters.IsBotiumTest|true 15 | 16 | #me 17 | book a room 18 | 19 | #bot 20 | BUTTONS Let's book a room. 21 | -------------------------------------------------------------------------------- /samples/Voice/botium.json: -------------------------------------------------------------------------------- 1 | { 2 | "botium": { 3 | "Capabilities": { 4 | "PROJECTNAME": "Botium Audio Sample", 5 | "CONTAINERMODE": "dialogflow", 6 | "DIALOGFLOW_PROJECT_ID": "botiumdemoroomreservation-jfdm", 7 | "DIALOGFLOW_AUDIOINPUT_ENCODING": "AUDIO_ENCODING_LINEAR_16", 8 | "DIALOGFLOW_AUDIOINPUT_SAMPLERATEHERTZ": 16000, 9 | "USER_INPUTS": [ 10 | { 11 | "ref": "MEDIA", 12 | "src": "MediaInput", 13 | "args": { 14 | "downloadMedia": true 15 | } 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/Voice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "mocha spec" 6 | }, 7 | "botium": { 8 | "convodirs": [ 9 | "spec/convo" 10 | ], 11 | "expandConvos": true, 12 | "expandUtterancesToConvos": false 13 | }, 14 | "devDependencies": { 15 | "botium-bindings": "latest", 16 | "botium-connector-dialogflow": "../..", 17 | "cross-env": "latest", 18 | "mocha": "latest" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/Voice/spec/botium.spec.js: -------------------------------------------------------------------------------- 1 | const bb = require('botium-bindings') 2 | bb.helper.mocha().setupMochaTestSuite() 3 | -------------------------------------------------------------------------------- /samples/Voice/spec/convo/audio.convo.txt: -------------------------------------------------------------------------------- 1 | audio 2 | 3 | #me 4 | MEDIA bookroom.wav 5 | 6 | #bot 7 | In what city would you like to reserve a room? 8 | 9 | #me 10 | London 11 | 12 | #bot 13 | -------------------------------------------------------------------------------- /samples/Voice/spec/convo/bookroom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforequity-at/botium-connector-dialogflow/02e49160a8dc971232415b5c0a1df92ca8199797/samples/Voice/spec/convo/bookroom.wav -------------------------------------------------------------------------------- /samples/assistant/botium.json: -------------------------------------------------------------------------------- 1 | { 2 | "botium": { 3 | "Capabilities": { 4 | "PROJECTNAME": "Google Assistant Sample", 5 | "CONTAINERMODE": "dialogflow", 6 | "DIALOGFLOW_PROJECT_ID": "room-reservation-cfa4c", 7 | "DIALOGFLOW_OUTPUT_PLATFORM": "ACTIONS_ON_GOOGLE" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/assistant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "botiumFluent.js", 6 | "scripts": { 7 | "test": "mocha spec", 8 | "import-intents": "botium-connector-dialogflow-cli dialogflowimport dialogflow-intents", 9 | "import-conversations": "botium-connector-dialogflow-cli dialogflowimport dialogflow-conversations" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "botium": { 15 | "convodirs": [ 16 | "spec\\convo" 17 | ], 18 | "expandConvos": true, 19 | "expandUtterancesToConvos": false 20 | }, 21 | "devDependencies": { 22 | "botium-bindings": "latest", 23 | "botium-connector-dialogflow": "../..", 24 | "mocha": "latest" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/assistant/spec/botium.spec.js: -------------------------------------------------------------------------------- 1 | const bb = require('botium-bindings') 2 | bb.helper.mocha().setupMochaTestSuite() 3 | -------------------------------------------------------------------------------- /samples/assistant/spec/convo/basiccard.convo.txt: -------------------------------------------------------------------------------- 1 | basiccard 2 | 3 | #me 4 | card 5 | 6 | #bot 7 | BUTTONS Botium Website 8 | MEDIA http://www.botium.at/images/logo.png 9 | -------------------------------------------------------------------------------- /samples/assistant/spec/convo/carousel.convo.txt: -------------------------------------------------------------------------------- 1 | carousel 2 | 3 | #me 4 | carousel 5 | 6 | #bot 7 | MEDIA http://www.botium.at/images/logo.png 8 | BUTTONS Botium1|Botium2 9 | -------------------------------------------------------------------------------- /samples/assistant/spec/convo/linkoutsuggestion.convo.txt: -------------------------------------------------------------------------------- 1 | linkoutsuggestion 2 | 3 | #me 4 | linkoutsuggestion 5 | 6 | #bot 7 | BUTTONS Botium Website 8 | 9 | -------------------------------------------------------------------------------- /samples/assistant/spec/convo/list.convo.txt: -------------------------------------------------------------------------------- 1 | list 2 | 3 | #me 4 | listselect 5 | 6 | #bot 7 | this is a listselect 8 | 9 | #bot 10 | My List 11 | MEDIA http://www.botium.at/images/logo.png 12 | BUTTONS Botium1|Botium2 13 | -------------------------------------------------------------------------------- /samples/assistant/spec/convo/simpleresponse.convo.txt: -------------------------------------------------------------------------------- 1 | simpleresponse 2 | 3 | #me 4 | simpleresponse 5 | 6 | #bot 7 | Hello, meat bag! 8 | -------------------------------------------------------------------------------- /samples/assistant/spec/convo/suggestions.convo.txt: -------------------------------------------------------------------------------- 1 | suggestions 2 | 3 | #me 4 | buttons 5 | 6 | #bot 7 | BUTTONS Quick Reply 1|Quick Reply 2|Quick Reply 3 8 | -------------------------------------------------------------------------------- /samples/banking/.gitignore: -------------------------------------------------------------------------------- 1 | spec/import -------------------------------------------------------------------------------- /samples/banking/botium.json: -------------------------------------------------------------------------------- 1 | { 2 | "botium": { 3 | "Capabilities": { 4 | "PROJECTNAME": "Google Dialogflow Techdemo", 5 | "CONTAINERMODE": "dialogflow", 6 | "DIALOGFLOW_PROJECT_ID": "banking-1-uejcap", 7 | "DIALOGFLOW_API_ENDPOINT": "global-dialogflow.googleapis.com" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/banking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "banking", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha spec", 8 | "import-utterances": "botium-connector-dialogflow-cli import --output spec/import", 9 | "import-intents": "botium-connector-dialogflow-cli import --buildconvos --output spec/import", 10 | "import-conversations": "botium-connector-dialogflow-cli import --buildmultistepconvos --output spec/import", 11 | "export": "botium-connector-dialogflow-cli export --input spec/import" 12 | }, 13 | "botium": { 14 | "convodirs": [ 15 | "spec/convo" 16 | ], 17 | "expandConvos": true, 18 | "expandUtterancesToConvos": false 19 | }, 20 | "devDependencies": { 21 | "mocha": "latest", 22 | "botium-bindings": "latest", 23 | "botium-connector-dialogflow": "../../" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/banking/spec/botium.spec.js: -------------------------------------------------------------------------------- 1 | const bb = require('botium-bindings') 2 | bb.helper.mocha().setupMochaTestSuite() 3 | -------------------------------------------------------------------------------- /samples/banking/spec/convo/asserters.convo.txt: -------------------------------------------------------------------------------- 1 | asserters 2 | 3 | #me 4 | hello 5 | UPDATE_CUSTOM SET_DIALOGFLOW_QUERYPARAMS|payload|{"key": "value"} 6 | 7 | #bot 8 | Hello 9 | INTENT Default Welcome Intent 10 | INTENT_CONFIDENCE 70 11 | 12 | #me 13 | check my credit card balance 14 | 15 | #bot 16 | Here's your latest balance 17 | INTENT account.balance.check 18 | INTENT_CONFIDENCE 80 19 | ENTITIES account 20 | ENTITY_VALUES credit card 21 | -------------------------------------------------------------------------------- /samples/banking/spec/convo/incomprehension.convo.txt: -------------------------------------------------------------------------------- 1 | incomprehension 2 | 3 | #me 4 | you wont understand this. muhahahaha! 5 | 6 | #bot 7 | INTENT Default Fallback Intent 8 | -------------------------------------------------------------------------------- /samples/banking/spec/convo/transfer.convo.txt: -------------------------------------------------------------------------------- 1 | transfer 2 | 3 | #me 4 | transfer money 5 | 6 | #bot 7 | Sure. Transfer from which account? 8 | INTENT transfer.money 9 | 10 | #me 11 | savings 12 | 13 | #bot 14 | To which account? 15 | INTENT transfer.money 16 | ENTITIES account-from.0 17 | ENTITY_VALUES savings account 18 | 19 | #me 20 | checking 21 | 22 | #bot 23 | And, how much do you want to transfer? 24 | INTENT transfer.money 25 | ENTITIES account-from.0|account-to 26 | ENTITY_VALUES savings account|checking account 27 | 28 | #me 29 | 10 USD 30 | 31 | #bot 32 | All right. So, you're transferring 10 USD from your savings account to a checking account. Is that right? 33 | INTENT transfer.money 34 | ENTITIES account-from.0|account-to|amount.amount|amount.currency 35 | ENTITY_VALUES savings account|checking account|10|USD 36 | 37 | -------------------------------------------------------------------------------- /src/dialogflowintents.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | const fs = require('fs') 3 | const JSZip = require('jszip') 4 | const dialogflow = require('@google-cloud/dialogflow') 5 | const _ = require('lodash') 6 | const botium = require('botium-core') 7 | const { convertToDialogflowUtterance, jsonBuffer } = require('./helpers') 8 | const debug = require('debug')('botium-connector-dialogflow-intents') 9 | 10 | const getUserSaysEntryNameForZipEntry = (zipEntry, language) => { 11 | const utterancesEntryName = zipEntry.name.replace('.json', '') + '_usersays_' + language + '.json' 12 | return utterancesEntryName 13 | } 14 | const getUserSaysEntryNameForIntent = (intentName, language) => { 15 | const filePrefix = intentName.replace(/:/g, '_') 16 | const utterancesEntryName = `intents/${filePrefix}_usersays_${language}.json` 17 | return utterancesEntryName 18 | } 19 | 20 | const selectLanguage = (agentInfo, caps) => { 21 | const agentLanguages = [agentInfo.language, ...(agentInfo.supportedLanguages || [])] 22 | 23 | const capsLanguage = caps.DIALOGFLOW_LANGUAGE_CODE 24 | if (capsLanguage) { 25 | if (agentLanguages.includes(capsLanguage)) return capsLanguage 26 | else throw new Error(`Language ${capsLanguage} not supported by this Dialogflow agent.`) 27 | } 28 | return agentInfo.language 29 | } 30 | 31 | const importIntents = async ({ agentInfo, zipEntries, unzip }, argv, { statusCallback }) => { 32 | const status = (log, obj) => { 33 | if (obj) debug(log, obj) 34 | else debug(log) 35 | if (statusCallback) statusCallback(log, obj) 36 | } 37 | const language = selectLanguage(agentInfo, argv.caps) 38 | status(`Using Dialogflow language "${language}"`) 39 | 40 | const intentEntries = zipEntries.filter((zipEntry) => zipEntry.name.startsWith('intent') && !zipEntry.name.match('usersays')) 41 | 42 | const convos = [] 43 | const utterances = [] 44 | 45 | for (const zipEntry of intentEntries) { 46 | const intent = JSON.parse(await unzip.file(zipEntry.name).async('string')) 47 | if (intent.parentId) continue 48 | 49 | const utterancesEntryName = getUserSaysEntryNameForZipEntry(zipEntry, language) 50 | debug(`Found root intent "${intent.name}", checking for utterances in ${utterancesEntryName}`) 51 | if (!zipEntries.find((zipEntry) => zipEntry.name === utterancesEntryName)) { 52 | status(`Utterances files not found for "${intent.name}", ignoring intent`) 53 | continue 54 | } 55 | const utterancesEntry = JSON.parse(await unzip.file(utterancesEntryName).async('string')) 56 | const inputUtterances = utterancesEntry.map((utterance) => utterance.data.reduce((accumulator, currentValue) => accumulator + '' + currentValue.text, '')) 57 | 58 | if (argv.buildconvos) { 59 | utterances.push({ 60 | name: intent.name, 61 | utterances: inputUtterances 62 | }) 63 | 64 | const convo = { 65 | header: { 66 | name: intent.name 67 | }, 68 | conversation: [ 69 | { 70 | sender: 'me', 71 | messageText: intent.name 72 | }, 73 | { 74 | sender: 'bot', 75 | asserters: [ 76 | { 77 | name: 'INTENT', 78 | args: [intent.name] 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | if (intent.contexts && intent.contexts.length > 0) { 85 | convo.conversation[0].logicHooks = intent.contexts.map(context => ({ 86 | name: 'UPDATE_CUSTOM', 87 | args: [ 88 | 'SET_DIALOGFLOW_CONTEXT', 89 | context, 90 | 1 91 | ] 92 | })) 93 | } 94 | 95 | convos.push(convo) 96 | } else { 97 | if (intent.contexts && intent.contexts.length > 0) { 98 | status(`Found intent requiring context ("${intent.name}": ${intent.contexts.join(',')}), ignoring intent`) 99 | } else { 100 | utterances.push({ 101 | name: intent.name, 102 | utterances: inputUtterances 103 | }) 104 | } 105 | } 106 | } 107 | return { convos, utterances } 108 | } 109 | 110 | const importConversations = async ({ agentInfo, zipEntries, unzip }, argv, { statusCallback }) => { 111 | const status = (log, obj) => { 112 | debug(log, obj) 113 | if (statusCallback) statusCallback(log, obj) 114 | } 115 | const language = selectLanguage(agentInfo, argv.caps) 116 | status(`Using Dialogflow language "${language}"`) 117 | 118 | const intentEntries = zipEntries.filter((zipEntry) => zipEntry.name.startsWith('intent') && !zipEntry.name.match('usersays')) 119 | 120 | const convos = [] 121 | const utterances = [] 122 | 123 | const intentsById = {} 124 | for (const zipEntry of intentEntries) { 125 | const intent = JSON.parse(await unzip.file(zipEntry.name).async('string')) 126 | 127 | const utterancesEntryName = getUserSaysEntryNameForZipEntry(zipEntry, language) 128 | debug(`Found intent ${intent.name}, checking for utterances in ${utterancesEntryName}`) 129 | if (!zipEntries.find((zipEntry) => zipEntry.name === utterancesEntryName)) { 130 | status(`Utterances files not found for ${intent.name}, ignoring intent`) 131 | continue 132 | } 133 | intentsById[intent.id] = intent 134 | 135 | const utterances = JSON.parse(await unzip.file(utterancesEntryName).async('string')) 136 | intent.inputUtterances = utterances.map((utterance) => utterance.data.reduce((accumulator, currentValue) => accumulator + '' + currentValue.text, '')) 137 | debug(`Utterances file for ${intent.name}: ${intent.inputUtterances}`) 138 | 139 | intent.outputUtterances = [] 140 | if (intent.responses) { 141 | intent.responses.forEach((response) => { 142 | if (response.messages) { 143 | const speechOutputs = response.messages 144 | .filter((message) => message.type === '0' && message.lang === agentInfo.language && message.speech) 145 | .reduce((acc, message) => { 146 | if (_.isArray(message.speech)) acc = acc.concat(message.speech) 147 | else acc.push(message.speech) 148 | return acc 149 | }, []) 150 | if (speechOutputs) { 151 | intent.outputUtterances.push(speechOutputs) 152 | } else { 153 | intent.outputUtterances.push([]) 154 | } 155 | } else { 156 | intent.outputUtterances.push([]) 157 | } 158 | }) 159 | } 160 | } 161 | Object.keys(intentsById).forEach((intentId) => { 162 | const intent = intentsById[intentId] 163 | debug(intent.name + '/' + intent.parentId) 164 | if (intent.parentId) { 165 | const parent = intentsById[intent.parentId] 166 | if (parent) { 167 | if (!parent.children) parent.children = [] 168 | parent.children.push(intent) 169 | } else { 170 | debug(`Parent intent with id ${intent.parentId} not found for ${intent.name}, ignoring intent`) 171 | } 172 | } 173 | }) 174 | Object.keys(intentsById).forEach((intentId) => { 175 | const intent = intentsById[intentId] 176 | if (intent.parentId) { 177 | delete intentsById[intentId] 178 | } 179 | }) 180 | 181 | const follow = (intent, currentStack = []) => { 182 | const cp = currentStack.slice(0) 183 | 184 | cp.push({ sender: 'me', messageText: intent.name, intent: intent.name }) 185 | 186 | utterances.push({ 187 | name: intent.name, 188 | utterances: intent.inputUtterances 189 | }) 190 | 191 | if (intent.outputUtterances && intent.outputUtterances.length > 0) { 192 | for (let stepIndex = 0; stepIndex < intent.outputUtterances.length; stepIndex++) { 193 | const convoStep = { 194 | sender: 'bot', 195 | asserters: [ 196 | { 197 | name: 'INTENT', 198 | args: [intent.name] 199 | } 200 | ] 201 | } 202 | if (intent.outputUtterances[stepIndex] && intent.outputUtterances[stepIndex].length > 0) { 203 | const utterancesRef = intent.name + ' - output ' + stepIndex 204 | utterances.push({ 205 | name: utterancesRef, 206 | utterances: intent.outputUtterances[stepIndex] 207 | }) 208 | convoStep.messageText = utterancesRef 209 | } 210 | cp.push(convoStep) 211 | } 212 | } else { 213 | cp.push({ sender: 'bot', messageText: '' }) 214 | } 215 | 216 | if (intent.children) { 217 | intent.children.forEach((child) => { 218 | follow(child, cp) 219 | }) 220 | } else { 221 | const convo = { 222 | header: { 223 | name: cp.filter((m) => m.sender === 'me').map((m) => m.intent).join(' - ') 224 | }, 225 | conversation: cp 226 | } 227 | debug(convo) 228 | convos.push(convo) 229 | } 230 | } 231 | Object.keys(intentsById).forEach((intentId) => follow(intentsById[intentId], [])) 232 | 233 | return { convos, utterances } 234 | } 235 | 236 | const loadAgentZip = async (filenameOrRawData) => { 237 | const result = { 238 | zipEntries: [] 239 | } 240 | if (_.isBuffer(filenameOrRawData)) { 241 | result.unzip = await JSZip.loadAsync(filenameOrRawData) 242 | } else { 243 | const buf = fs.readFileSync(filenameOrRawData) 244 | result.unzip = await JSZip.loadAsync(buf) 245 | } 246 | result.unzip.forEach((relativePath, zipEntry) => { 247 | result.zipEntries.push(zipEntry) 248 | debug(`Dialogflow agent got entry: ${zipEntry.name}`) 249 | }) 250 | result.agentInfo = JSON.parse(await result.unzip.file('agent.json').async('string')) 251 | debug(`Dialogflow agent info: ${util.inspect(result.agentInfo)}`) 252 | return result 253 | } 254 | 255 | const importDialogflow = async (argv, status, importFunction) => { 256 | const caps = argv.caps || {} 257 | const driver = new botium.BotDriver(caps) 258 | const container = await driver.Build() 259 | 260 | let agent = null 261 | try { 262 | if (!argv.agentzip) { 263 | try { 264 | debug('Building Dialogflow Connection with sessionOpts', container.pluginInstance.sessionOpts) 265 | const agentsClient = new dialogflow.AgentsClient(container.pluginInstance.sessionOpts) 266 | debug('Building Dialogflow Connection with projectPath', container.pluginInstance.caps.DIALOGFLOW_PROJECT_ID) 267 | const projectPath = agentsClient.projectPath(container.pluginInstance.caps.DIALOGFLOW_PROJECT_ID) 268 | 269 | const allResponses = await agentsClient.exportAgent({ parent: projectPath }) 270 | const responses = await allResponses[0].promise() 271 | try { 272 | const buf = Buffer.from(responses[0].agentContent, 'base64') 273 | agent = await loadAgentZip(buf) 274 | } catch (err) { 275 | throw new Error(`Dialogflow agent unpack failed: ${err && err.message}`) 276 | } 277 | } catch (err) { 278 | throw new Error(`Dialogflow agent connection failed: ${util.inspect(err)}`) 279 | } 280 | } else { 281 | try { 282 | agent = await loadAgentZip(argv.agentzip) 283 | } catch (err) { 284 | throw new Error(`Dialogflow agent unpack failed: ${err && err.message}`) 285 | } 286 | } 287 | const { convos, utterances } = await importFunction(agent, { ...argv, caps: container.pluginInstance.caps }, status) 288 | return { convos, utterances } 289 | } finally { 290 | try { 291 | await container.Clean() 292 | } catch (err) { 293 | debug(`Error container cleanup: ${err && err.message}`) 294 | } 295 | } 296 | } 297 | 298 | const importHandler = async (argv, status) => { 299 | debug(`command options: ${util.inspect(argv)}`) 300 | 301 | let result = null 302 | if (argv.buildmultistepconvos) { 303 | result = await importDialogflow(argv, status, importConversations) 304 | } else { 305 | result = await importDialogflow(argv, status, importIntents) 306 | } 307 | return { 308 | convos: result.convos, 309 | utterances: result.utterances 310 | } 311 | } 312 | 313 | const exportHandler = async ({ caps, getzip, agentzip, output, ...rest }, { utterances, convos }, { statusCallback }) => { 314 | caps = caps || {} 315 | const driver = new botium.BotDriver(caps) 316 | const container = await driver.Build() 317 | 318 | const status = (log, obj) => { 319 | if (obj) debug(log, obj) 320 | else debug(log) 321 | if (statusCallback) statusCallback(log, obj) 322 | } 323 | 324 | let agent = null 325 | try { 326 | if (!agentzip) { 327 | try { 328 | debug('Building Dialogflow Connection with sessionOpts', container.pluginInstance.sessionOpts) 329 | const agentsClient = new dialogflow.AgentsClient(container.pluginInstance.sessionOpts) 330 | debug('Building Dialogflow Connection with projectPath', container.pluginInstance.caps.DIALOGFLOW_PROJECT_ID) 331 | const projectPath = agentsClient.projectPath(container.pluginInstance.caps.DIALOGFLOW_PROJECT_ID) 332 | 333 | const allResponses = await agentsClient.exportAgent({ parent: projectPath }) 334 | const responses = await allResponses[0].promise() 335 | try { 336 | const buf = Buffer.from(responses[0].agentContent, 'base64') 337 | agent = await loadAgentZip(buf) 338 | } catch (err) { 339 | throw new Error(`Dialogflow agent unpack failed: ${err && err.message}`) 340 | } 341 | } catch (err) { 342 | throw new Error(`Dialogflow agent connection failed: ${util.inspect(err)}`) 343 | } 344 | } else { 345 | try { 346 | agent = await loadAgentZip(agentzip) 347 | } catch (err) { 348 | throw new Error(`Dialogflow agent unpack failed: ${err && err.message}`) 349 | } 350 | } 351 | 352 | const language = selectLanguage(agent.agentInfo, container.pluginInstance.caps) 353 | status(`Using Dialogflow language "${language}"`) 354 | 355 | for (const utt of utterances) { 356 | const utterancesEntryName = getUserSaysEntryNameForIntent(utt.name, language) 357 | const utterancesEntry = agent.zipEntries.find((zipEntry) => zipEntry.name === utterancesEntryName) 358 | 359 | let utterancesEntryContent = [] 360 | if (utterancesEntry) { 361 | utterancesEntryContent = JSON.parse(await agent.unzip.file(utterancesEntryName).async('string')) 362 | } else { 363 | status(`User examples file ${utterancesEntryName} not found for "${utt.name}", creating a new one`) 364 | } 365 | 366 | const agentExamples = utterancesEntryContent.map((utterance) => utterance.data.reduce((accumulator, currentValue) => accumulator + '' + currentValue.text, '')) 367 | const newExamples = utt.utterances.filter(u => !agentExamples.includes(u)) 368 | if (newExamples.length === 0) { 369 | status(`No new user examples files found for "${utt.name}".`) 370 | } else { 371 | status(`${newExamples.length} new user examples found for "${utt.name}", adding to agent`) 372 | 373 | const newData = convertToDialogflowUtterance(newExamples, language) 374 | agent.unzip.file(utterancesEntryName, jsonBuffer(utterancesEntryContent.concat(newData))) 375 | } 376 | } 377 | const agentZipBuffer = await agent.unzip.generateAsync({ type: 'nodebuffer' }) 378 | if (getzip) { 379 | return { agentZipBuffer } 380 | } else if (output) { 381 | fs.writeFileSync(output, agentZipBuffer) 382 | return { agentInfo: agent.agentInfo } 383 | } else { 384 | try { 385 | debug('Building Dialogflow Connection with sessionOpts', container.pluginInstance.sessionOpts) 386 | const agentsClient = new dialogflow.AgentsClient(container.pluginInstance.sessionOpts) 387 | debug('Building Dialogflow Connection with projectPath', container.pluginInstance.caps.DIALOGFLOW_PROJECT_ID) 388 | const projectPath = agentsClient.projectPath(container.pluginInstance.caps.DIALOGFLOW_PROJECT_ID) 389 | const agentResponses = await agentsClient.getAgent({ parent: projectPath }) 390 | const newAgentInfo = agentResponses[0] 391 | debug('Uploading and restoring Dialogflow agent', newAgentInfo) 392 | const restoreResponses = await agentsClient.restoreAgent({ parent: newAgentInfo.parent, agentContent: agentZipBuffer }) 393 | await restoreResponses[0].promise() 394 | status(`Uploaded Dialogflow Agent to ${projectPath}`, newAgentInfo) 395 | return { agentInfo: newAgentInfo } 396 | } catch (err) { 397 | throw new Error(`Dialogflow agent connection failed: ${util.inspect(err)}`) 398 | } 399 | } 400 | } finally { 401 | if (container) { 402 | try { 403 | await container.Clean() 404 | } catch (err) { 405 | debug(`Error container cleanup: ${err && err.message}`) 406 | } 407 | } 408 | } 409 | } 410 | 411 | module.exports = { 412 | importHandler: ({ caps, buildconvos, buildmultistepconvos, agentzip, ...rest } = {}, { statusCallback } = {}) => importHandler({ caps, buildconvos, buildmultistepconvos, agentzip, ...rest }, { statusCallback }), 413 | importArgs: { 414 | caps: { 415 | describe: 'Capabilities', 416 | type: 'json', 417 | skipCli: true 418 | }, 419 | buildconvos: { 420 | describe: 'Build convo files with intent asserters', 421 | type: 'boolean', 422 | default: false 423 | }, 424 | buildmultistepconvos: { 425 | describe: 'Reverse-engineer Dialogflow agent and build multi-step convo files', 426 | type: 'boolean', 427 | default: false 428 | }, 429 | agentzip: { 430 | describe: 'Path to the exported Dialogflow agent zip file. If not given, it will be downloaded (with connection settings from botium.json).', 431 | type: 'string' 432 | } 433 | }, 434 | exportHandler: ({ caps, getzip, agentzip, output, ...rest } = {}, { convos, utterances } = {}, { statusCallback } = {}) => exportHandler({ caps, getzip, agentzip, output, ...rest }, { convos, utterances }, { statusCallback }), 435 | exportArgs: { 436 | caps: { 437 | describe: 'Capabilities', 438 | type: 'json', 439 | skipCli: true 440 | }, 441 | getzip: { 442 | describe: 'Return ZIP file buffer', 443 | type: 'boolean', 444 | skipCli: true, 445 | default: false 446 | }, 447 | agentzip: { 448 | describe: 'Path to the exported Dialogflow agent zip file. If not given, it will be downloaded (with connection settings from botium.json).', 449 | type: 'string' 450 | }, 451 | output: { 452 | describe: 'Path to the changed Dialogflow agent zip file.', 453 | type: 'string' 454 | } 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid') 2 | const JSZip = require('jszip') 3 | 4 | const jsonBuffer = (obj) => { 5 | return Buffer.from(JSON.stringify(obj, null, 2), 'utf-8') 6 | } 7 | 8 | const convertToDialogflowUtterance = (examples, language) => { 9 | return examples.map(utt => { 10 | return { 11 | id: uuidv4(), 12 | data: [ 13 | { 14 | text: utt, 15 | userDefined: false 16 | } 17 | ], 18 | isTemplate: false, 19 | lang: language, 20 | count: 0, 21 | updated: 0 22 | } 23 | }) 24 | } 25 | 26 | const loadAgentZip = async (agentsClient, projectPath) => { 27 | const agentResponses = await agentsClient.getAgent({ parent: projectPath }) 28 | const agentInfo = agentResponses[0] 29 | const exportResponses = await agentsClient.exportAgent({ parent: projectPath }) 30 | const waitResponses = await exportResponses[0].promise() 31 | try { 32 | const buf = Buffer.from(waitResponses[0].agentContent, 'base64') 33 | 34 | const unzip = await JSZip.loadAsync(buf) 35 | const zipEntries = [] 36 | unzip.forEach((relativePath, zipEntry) => { 37 | zipEntries.push(zipEntry) 38 | }) 39 | 40 | return { 41 | unzip, 42 | zipEntries, 43 | agentInfo 44 | } 45 | } catch (err) { 46 | throw new Error(`Dialogflow agent unpack failed: ${err.message}`) 47 | } 48 | } 49 | 50 | module.exports = { 51 | jsonBuffer, 52 | convertToDialogflowUtterance, 53 | loadAgentZip 54 | } 55 | -------------------------------------------------------------------------------- /src/nlp.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid') 2 | const path = require('path') 3 | const randomize = require('randomatic') 4 | const JSZip = require('jszip') 5 | const dialogflow = require('@google-cloud/dialogflow') 6 | const botium = require('botium-core') 7 | const debug = require('debug')('botium-connector-dialogflow-nlp') 8 | 9 | const { loadAgentZip } = require('./helpers') 10 | 11 | const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)) 12 | 13 | const getCaps = (caps) => { 14 | const result = Object.assign({}, caps || {}) 15 | result.CONTAINERMODE = path.resolve(__dirname, '..', 'index.js') 16 | result.DIALOGFLOW_FORCE_INTENT_RESOLUTION = true 17 | return result 18 | } 19 | 20 | const getNLPCaps = (caps) => { 21 | const result = Object.assign({}, caps || {}) 22 | result.CONTAINERMODE = path.resolve(__dirname, '..', 'index.js') 23 | result.DIALOGFLOW_FORCE_INTENT_RESOLUTION = true 24 | result.DIALOGFLOW_PROJECT_ID = caps.DIALOGFLOW_NLP_PROJECT_ID 25 | result.DIALOGFLOW_CLIENT_EMAIL = caps.DIALOGFLOW_NLP_CLIENT_EMAIL 26 | result.DIALOGFLOW_PRIVATE_KEY = caps.DIALOGFLOW_NLP_PRIVATE_KEY 27 | return result 28 | } 29 | 30 | const jsonBuffer = (obj) => { 31 | return Buffer.from(JSON.stringify(obj, null, 2), 'utf-8') 32 | } 33 | 34 | const extractIntentUtterances = async ({ caps }) => { 35 | const driver = new botium.BotDriver(getCaps(caps)) 36 | const container = await driver.Build() 37 | 38 | try { 39 | const agentsClient = new dialogflow.AgentsClient(container.pluginInstance.sessionOpts) 40 | const projectPath = agentsClient.projectPath(container.caps.DIALOGFLOW_PROJECT_ID) 41 | const { unzip, zipEntries, agentInfo } = await loadAgentZip(agentsClient, projectPath) 42 | debug(`Dialogflow agent: ${JSON.stringify(agentInfo, null, 2)}`) 43 | debug(`Dialogflow agent files: ${JSON.stringify(zipEntries.map(z => z.name), null, 2)}`) 44 | 45 | const languageCodeBotium = container.pluginInstance.caps.DIALOGFLOW_LANGUAGE_CODE.toLowerCase() 46 | const languageCodeAgent = agentInfo.defaultLanguageCode 47 | 48 | const intents = [] 49 | 50 | const intentEntries = zipEntries.filter((zipEntry) => zipEntry.name.startsWith('intent') && !zipEntry.name.match('usersays')) 51 | for (const zipEntry of intentEntries) { 52 | const intent = JSON.parse(await unzip.file(zipEntry.name).async('string')) 53 | if (intent.parentId) continue 54 | if (intent.contexts && intent.contexts.length > 0) continue 55 | 56 | const utterancesEntryName1 = zipEntry.name.replace('.json', '') + '_usersays_' + languageCodeBotium + '.json' 57 | const utterancesEntryName2 = zipEntry.name.replace('.json', '') + '_usersays_' + languageCodeAgent + '.json' 58 | 59 | let utterancesEntryName = null 60 | if (zipEntries.find((zipEntry) => zipEntry.name === utterancesEntryName1)) { 61 | utterancesEntryName = utterancesEntryName1 62 | } else if (zipEntries.find((zipEntry) => zipEntry.name === utterancesEntryName2)) { 63 | utterancesEntryName = utterancesEntryName2 64 | } 65 | if (!utterancesEntryName) { 66 | debug(`Utterances files not found for ${intent.name}, checking for utterances in ${utterancesEntryName1} and ${utterancesEntryName2}. Ignoring intent.`) 67 | } else { 68 | const utterancesEntry = JSON.parse(await unzip.file(utterancesEntryName).async('string')) 69 | const inputUtterances = utterancesEntry.map((utterance) => utterance.data.reduce((accumulator, currentValue) => accumulator + '' + currentValue.text, '')) 70 | 71 | intents.push({ 72 | intentName: intent.name, 73 | utterances: inputUtterances 74 | }) 75 | } 76 | } 77 | return { 78 | intents, 79 | origAgentInfo: agentInfo 80 | } 81 | } finally { 82 | if (container) await container.Clean() 83 | } 84 | } 85 | 86 | const trainIntentUtterances = async ({ caps }, intents, { origAgentInfo }) => { 87 | const driver = new botium.BotDriver(getCaps(caps)) 88 | 89 | if (!driver.caps.DIALOGFLOW_NLP_PROJECT_ID || !driver.caps.DIALOGFLOW_NLP_CLIENT_EMAIL || !driver.caps.DIALOGFLOW_NLP_PRIVATE_KEY) { 90 | throw new Error('Required to create separate Google Project for Training and set capabilities DIALOGFLOW_NLP_PROJECT_ID + DIALOGFLOW_NLP_CLIENT_EMAIL + DIALOGFLOW_NLP_PRIVATE_KEY') 91 | } 92 | 93 | const nlpDriver = new botium.BotDriver(getNLPCaps(driver.caps)) 94 | const nlpContainer = await nlpDriver.Build() 95 | 96 | try { 97 | const agentsClient = new dialogflow.AgentsClient(nlpContainer.pluginInstance.sessionOpts) 98 | const projectPathNLP = agentsClient.projectPath(nlpContainer.pluginInstance.caps.DIALOGFLOW_NLP_PROJECT_ID) 99 | 100 | const newAgentData = { 101 | parent: projectPathNLP, 102 | enableLogging: true, 103 | timeZone: 'Europe/Madrid' 104 | } 105 | if (origAgentInfo) { 106 | Object.assign(newAgentData, { 107 | displayName: `${origAgentInfo.displayName}-BotiumTrainingCopy-${randomize('Aa0', 5)}`, 108 | defaultLanguageCode: origAgentInfo.defaultLanguageCode, 109 | timeZone: origAgentInfo.timeZone, 110 | matchMode: origAgentInfo.matchMode, 111 | classificationThreshold: origAgentInfo.classificationThreshold 112 | }) 113 | } else { 114 | Object.assign(newAgentData, { 115 | displayName: `BotiumTrainingCopy-${randomize('Aa0', 5)}`, 116 | defaultLanguageCode: nlpContainer.pluginInstance.caps.DIALOGFLOW_LANGUAGE_CODE 117 | }) 118 | } 119 | 120 | const createAgentResponses = await agentsClient.setAgent({ agent: newAgentData }) 121 | const newAgent = createAgentResponses[0] 122 | debug(`Dialogflow agent created: ${newAgent.parent}/${newAgent.displayName}`) 123 | 124 | const agentZip = new JSZip() 125 | agentZip.file('package.json', jsonBuffer({ 126 | version: '1.0.0' 127 | })) 128 | agentZip.file('agent.json', jsonBuffer({ 129 | language: newAgentData.defaultLanguageCode, 130 | defaultTimezone: newAgentData.timeZone 131 | })) 132 | const agentZipIntentFolder = agentZip.folder('intents') 133 | for (const intent of (intents || [])) { 134 | agentZipIntentFolder.file(`${intent.intentName}.json`, jsonBuffer({ 135 | id: uuidv4(), 136 | name: intent.intentName 137 | })) 138 | agentZipIntentFolder.file(`${intent.intentName}_usersays_${newAgent.defaultLanguageCode}.json`, jsonBuffer( 139 | (intent.utterances || []).map(u => ({ 140 | id: uuidv4(), 141 | data: [ 142 | { 143 | text: u 144 | } 145 | ] 146 | })) 147 | )) 148 | } 149 | const agentZipBuffer = await agentZip.generateAsync({ type: 'nodebuffer' }) 150 | 151 | debug(`Dialogflow agent restoring intents: ${newAgent.parent}/${newAgent.displayName}`) 152 | const restoreResponses = await agentsClient.restoreAgent({ parent: newAgent.parent, agentContent: agentZipBuffer }) 153 | await restoreResponses[0].promise() 154 | 155 | debug(`Dialogflow agent started training: ${newAgent.parent}/${newAgent.displayName}`) 156 | const trainResponses = await agentsClient.trainAgent({ parent: newAgent.parent }) 157 | await trainResponses[0].promise() 158 | 159 | debug(`Dialogflow agent ready: ${newAgent.parent}/${newAgent.displayName}`) 160 | 161 | const sessionClient = new dialogflow.SessionsClient(nlpContainer.pluginInstance.sessionOpts) 162 | const sessionPath = sessionClient.projectAgentSessionPath(nlpContainer.pluginInstance.caps.DIALOGFLOW_NLP_PROJECT_ID, randomize('Aa0', 20)) 163 | const pingRequest = { 164 | session: sessionPath, 165 | queryInput: { 166 | text: { 167 | text: 'hello', 168 | languageCode: nlpContainer.pluginInstance.caps.DIALOGFLOW_LANGUAGE_CODE 169 | } 170 | } 171 | } 172 | while (true) { 173 | try { 174 | await sessionClient.detectIntent(pingRequest) 175 | debug(`Dialogflow agent ${newAgent.parent}/${newAgent.displayName} returned response on pingRequest, continue.`) 176 | break 177 | } catch (err) { 178 | debug(`Dialogflow agent ${newAgent.parent}/${newAgent.displayName} failed on pingRequest, waiting ... (${err.message})`) 179 | await timeout(2000) 180 | } 181 | } 182 | 183 | return { 184 | caps: Object.assign({}, nlpContainer.pluginInstance.caps), 185 | origAgentInfo, 186 | tempAgent: newAgent 187 | } 188 | } finally { 189 | if (nlpContainer) await nlpContainer.Clean() 190 | } 191 | } 192 | 193 | const cleanupIntentUtterances = async ({ caps }, { caps: trainCaps, origAgentInfo, tempAgent }) => { 194 | } 195 | 196 | module.exports = { 197 | extractIntentUtterances, 198 | trainIntentUtterances, 199 | cleanupIntentUtterances 200 | } 201 | -------------------------------------------------------------------------------- /structJson.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var _a; 3 | exports.__esModule = true; 4 | exports.list = exports.struct = exports.value = void 0; 5 | /** 6 | * Valid `kind` types 7 | */ 8 | var Kind; 9 | (function (Kind) { 10 | Kind["Struct"] = "structValue"; 11 | Kind["List"] = "listValue"; 12 | Kind["Number"] = "numberValue"; 13 | Kind["String"] = "stringValue"; 14 | Kind["Bool"] = "boolValue"; 15 | Kind["Null"] = "nullValue"; 16 | })(Kind || (Kind = {})); 17 | var toString = Object.prototype.toString; 18 | var encoders = (_a = {}, 19 | _a[typeOf({})] = function (v) { return wrap(Kind.Struct, exports.struct.encode(v)); }, 20 | _a[typeOf([])] = function (v) { return wrap(Kind.List, exports.list.encode(v)); }, 21 | _a[typeOf(0)] = function (v) { return wrap(Kind.Number, v); }, 22 | _a[typeOf('')] = function (v) { return wrap(Kind.String, v); }, 23 | _a[typeOf(true)] = function (v) { return wrap(Kind.Bool, v); }, 24 | _a[typeOf(null)] = function () { return wrap(Kind.Null, 0); }, 25 | _a); 26 | function typeOf(value) { 27 | return toString.call(value); 28 | } 29 | function wrap(kind, value) { 30 | var _a; 31 | return _a = { kind: kind }, _a[kind] = value, _a; 32 | } 33 | function getKind(value) { 34 | if (value.kind) { 35 | return value.kind; 36 | } 37 | var validKinds = Object.values(Kind); 38 | for (var _i = 0, validKinds_1 = validKinds; _i < validKinds_1.length; _i++) { 39 | var kind = validKinds_1[_i]; 40 | if (value.hasOwnProperty(kind)) { 41 | return kind; 42 | } 43 | } 44 | return null; 45 | } 46 | /** 47 | * Used to encode/decode {@link Value} objects. 48 | */ 49 | exports.value = { 50 | /** 51 | * Encodes a JSON value into a protobuf {@link Value}. 52 | * 53 | * @param {*} value The JSON value. 54 | * @returns {Value} 55 | */ 56 | encode: function (value) { 57 | var type = typeOf(value); 58 | var encoder = encoders[type]; 59 | if (typeof encoder !== 'function') { 60 | throw new TypeError("Unable to infer type for \"" + value + "\"."); 61 | } 62 | return encoder(value); 63 | }, 64 | /** 65 | * Decodes a protobuf {@link Value} into a JSON value. 66 | * 67 | * @throws {TypeError} If unable to determine value `kind`. 68 | * 69 | * @param {Value} value the protobuf value. 70 | * @returns {*} 71 | */ 72 | decode: function (value) { 73 | var kind = getKind(value); 74 | if (!kind) { 75 | throw new TypeError("Unable to determine kind for \"" + value + "\"."); 76 | } 77 | switch (kind) { 78 | case 'listValue': 79 | return exports.list.decode(value.listValue); 80 | case 'structValue': 81 | return exports.struct.decode(value.structValue); 82 | case 'nullValue': 83 | return null; 84 | default: 85 | return value[kind]; 86 | } 87 | } 88 | }; 89 | /** 90 | * Used to encode/decode {@link Struct} objects. 91 | */ 92 | exports.struct = { 93 | /** 94 | * Encodes a JSON object into a protobuf {@link Struct}. 95 | * 96 | * @param {Object.} value the JSON object. 97 | * @returns {Struct} 98 | */ 99 | encode: function (json) { 100 | var fields = {}; 101 | Object.keys(json).forEach(function (key) { 102 | // If value is undefined, do not encode it. 103 | if (typeof json[key] === 'undefined') 104 | return; 105 | fields[key] = exports.value.encode(json[key]); 106 | }); 107 | return { fields: fields }; 108 | }, 109 | /** 110 | * Decodes a protobuf {@link Struct} into a JSON object. 111 | * 112 | * @param {Struct} struct the protobuf struct. 113 | * @returns {Object.} 114 | */ 115 | decode: function (_a) { 116 | var fields = _a.fields || {}; 117 | var json = {}; 118 | Object.keys(fields).forEach(function (key) { 119 | json[key] = exports.value.decode(fields[key]); 120 | }); 121 | return json; 122 | } 123 | }; 124 | /** 125 | * Used to encode/decode {@link ListValue} objects. 126 | */ 127 | exports.list = { 128 | /** 129 | * Encodes an array of JSON values into a protobuf {@link ListValue}. 130 | * 131 | * @param {Array.<*>} values the JSON values. 132 | * @returns {ListValue} 133 | */ 134 | encode: function (values) { 135 | return { 136 | values: (values || []).map(exports.value.encode) 137 | }; 138 | }, 139 | /** 140 | * Decodes a protobuf {@link ListValue} into an array of JSON values. 141 | * 142 | * @param {ListValue} list the protobuf list value. 143 | * @returns {Array.<*>} 144 | */ 145 | decode: function (_a) { 146 | var values = _a.values || [] 147 | return values.map(exports.value.decode); 148 | } 149 | }; 150 | //# sourceMappingURL=index.js.map --------------------------------------------------------------------------------