@manekinekko/google-actions-server

Google actions Node.js server

Usage no npm install needed!

<script type="module">
  import manekinekkoGoogleActionsServer from 'https://cdn.skypack.dev/@manekinekko/google-actions-server';
</script>

README

GAS: Google Assistant Server

A Node.js server for your Google Assistant (and Google Home).

Get it from NPM

npm i -S @manekinekko/google-actions-server

Get it from Yarn

$ yarn add @manekinekko/google-actions-server

How to use it

See the GAS Starter for more details.

GAS API

Here is the simplest guide in order to use GAS:

import { ActionServer } from '@manekinekko/google-actions-server';

const agent = new ActionServer();

agent.welcome((assistant) => {
    agent.ask('Hello Home. How can I help');
});

agent.intent(ActionServer.intent.action.TEXT, (assistant) => {

  // reads the user's request
  const userInput = assistant.getRawInput();

})

// start listening for commands (on port 8080)
agent.listen();

Intents

Available intents are:

ActionServer.intent.action.MAIN

This intent is triggered when users invoke your action by name, such as "talk to ". This intent si required for every action package.

ActionServer.intent.action.TEXT

This intent is triggered when users speak input and you receive the input as a request to your fulfillment endpoint.

ActionServer.intent.action.PERMISSION

Triggered if Google Assistant needs to ask the user for more permissions (not implemented yet by GAS)

ActionServer

ActionServer(port = 8080)

Create an HTTP server and set it to listen on port port (default: 8080).

Agent

Calling new ActionServer() will return an Agent instance, which exposes the following API:

agent.intent(name, callback)

Register a callback with an intent of type ActionServer.intent.action.*. When invoked, the callback function receives an instance of ActionsSdkAssistant. For instance:

agent.welcome(ActionServer.intent.action.TEXT, (assistant) => {
  // assistant
});

agent.welcome(callback)

Register a callback with the ActionServer.intent.action.MAIN intent. This is the same as:

agent.intent(ActionServer.intent.action.MAIN, (assistant) => {
  // assistant
});

Register a callback with the ActionServer.intent.action.MAIN intent. This is the same as:

agent.listen()

Starts the GAS server on port 8080 (the default port). This method must be called after you have registered all of your intents.

agent.ask(message)

A convenient method that abstracts away the assistant.ask() configuration. Typically, with assistant.ask() you have to:

const inputPrompt = assistant.buildInputPrompt(isSsml, message, noInputs);
assistant.data = previousState;
assistant.ask(inputPrompt);

With agent.ask(), you can just pass it the message:

agent.ask(message);

Of course, you can still call assistant.ask() if you want to provide a configuration.

setGreetings(greetings)

A convenient method to set a list of greeting messages that you will play randomly to the user.

getRandomGreeting()

A convenient method to get a random greeting message set using agent.setGreetings().

randomGreeting()

A convenient method that will trigger a random greeting message for you.

setConversationMessages(conversationMessages)

A convenient method to set a list of random conversations asking the user for another query. For instant: How can I help? or Do you have another request?.

getRandomConversationMessage()

A convenient method to get a random conversation message set using agent.setConversationMessages().

agent.train(dataSetKey, data, fields = ['data']))

This method is used to provide a set of data that will be used by agent.matchUserRequest(). This could be the data that will be used by the assistant to interact with the user, or a set of Hot Words such as:

this.agent.train('hot_words', [
    'what time is it?',
    'tell me the time',
    `what's the time`,
    //...
]);

Here is another use case, let's say you want to provide a decision list:

const decisionList = [
    {
        'scenario': 'I have one existing Observable, and I want to change each emitted value to be a constant value',
        'operator': 'mapTo'
    },
    {
        'scenario': 'I have one existing Observable, and I want to change each emitted value to be a value calculated through a formula',
        'operator': 'map'
    },
    ...
];

You would then provide it to GAS like so:

const fields = ['operator', 'scenario'];
this.agent.train('decision_list', decisionList, fields);

Please note that the fields option has been provided since the items inside this decision list are stored in key/value pairs.

IMPORTANT: the first field (in the sample above: operator) will be used as the reference field: the field that will be returned by agent.matchUserRequest().

matchUserRequest(dataSetKey, rawInput, responseCallback, lookupOptions = {})

NOTE: GAS uses elasticlunr that performs the Full-Text Search technique to retrieve the matched documents.

Use this method to match the user's input with a set of data provided in agent.train().

For instance, let's say you have the decisionList list from the example above; in order to find a matched document you would call agent.matchUserRequest() like so:

// get the user's input
const rawInput = assistant.getRawInput();

// provide a lookup configuration (see below)
const lookupOptions = {...};

// the callback that will be called
const responseCallback = (foundDocuments, rawInput) => { ... };

// run the search operation
const matchedDocuments = this.agent.matchUserRequest('decision_list', rawInput, responseCallback, lookupOptions);

The matchedDocuments would then contain the matched documents based on the rawInput string. In some case you may get false-positives, documents that are not relevant to the intended search question. In this case, you may provide a lookupOptions object in order to configure/tweak the search operation.

NOTE: if no documents are found, an empty array is returned.

threshold

You can provide the threshold property to filter out the false-positives. threshold can be either a Number or a Function.

Using a number:

const lookupOptions = {
    threshold: 0.6
};

When using a function, it will be provided with the current entry from the data set. You can access the score property on that entry:

const lookupOptions = {
    threshold: entry => {
        return entry.score > 0.6;
    }
};
fields

The fields property is a elasticlunr configuration query. For instance, we could provide the following configuration for the example from above:

const lookupOptions = {
    fields: {
        scenario: {
            boost: 2,
            bool: 'AND',
            expand: true
        },
        operator: {
            boost: 1
        }
    }
};

See elasticlunr configuration query for more details.

fetch(url, responseCallback, useCache = true)

A convenient method to fetch a remote document. Useful if you want to get more information from a remote website. For instance:

agent.fetch(url, (error, content) => {
    if (error) {
        console.error(error);
    }
    else {
        console.log(content);
    }
});

Disclaimer

The current implementation is still a POC. It's obviously missing lots of features. Please open issues to discuss any feature you want to be added, submit suggestions, or anything else... All contributions are welcome ;)

License

The MIT License (MIT) Copyright (c) 2017 - Wassim CHEGHAM

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.