google-actions-server icon indicating copy to clipboard operation
google-actions-server copied to clipboard

A Node.js server for your Google Actions

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('Welcome 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 <YOUR ACTION NAME HERE>". 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 ;)