Last month we brought Artsy to Amazon Alexa. In this post we'll repeat that process and enable Artsy for Google Assistant, including Google Home.

tl;dr You can try Artsy on your Google Home now, say "Ok Google, talk to Artsy" or see assistant.artsy.net for more info.

Conversation Actions

Actions are two-way dialogs with users. A user invokes an action, which calls so-called fulfillment code - a REST endpoint.

If you've never worked with Google Actions, read the conversation actions intro.

Actions SDK

To write an action get the actions SDK, which is a node.js client library, describe the actions in JSON and use a command-line tool called gactions or a web simulator to invoke the action code.

Actions Server

The actions SDK can be called from any node.js application, or with some help from google-actions-server (GAS) via boilerplate code in the google-actions-starter library.

An action.json that describes the actions, including the invocation trigger for each action and the text-to-speech voice for the agent to use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "versionLabel": "1.0.0",
  "agentInfo": {
    "languageCode": "en-US",
    "projectId": "biesenbach-one",
    "invocationNames": ["artsy"],
    "voiceName": "female_1"
  },
  "actions": [{
    "initialTrigger": {
      "intent": "assistant.intent.action.MAIN"
    },
    "httpExecution": {
      "url": "https://biesenbach-one.appspot-preview.com"
    }
  }]
}

The action implementation lives in lib/action.js that imports and creates a new instance of google-actions-server, binds intents to functions and issues questions with agent.ask or sends final responses with assistant.tell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { ActionServer } from '@manekinekko/google-actions-server';

class ArtsyAction {
  constructor() {
    this.agent = new ActionServer();

    this.agent.setGreetings([
      `What is the name of the artist you would like to hear about?`
    ]);
  }

  welcomeIntent(assistant) {
    return this.agent.randomGreeting();
  }

  textIntent(assistant) {
    var query = assistant.getRawInput();

    // TODO: respond to a query

    assistant.tell('You said ' + query + '.');
  }

  listen() {
    // the welcome intent is invoked when the user says "talk to Artsy"
    this.agent.welcome(this.welcomeIntent.bind(this));
    // the text action is invoked for any spoken text
    this.agent.intent(ActionServer.intent.action.TEXT, this.textIntent.bind(this));
    return this.agent.listen();
  }
}

module.exports = (new ArtsyAction()).listen();

With tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
let request = require('supertest')

describe('Artsy', () => {
  let action;

  beforeEach(() => {
    action = require('../action');
  });

  afterEach(() => {
    action.close();
  });

  it('asks the name of the artist when launched', () => {
    return request(action)
      .post('/')
      .send({
        inputs: [{
          intent: 'assistant.intent.action.MAIN',
          raw_inputs: [{
            input_type: 2,
            query: "OK Google, talk to Artsy."
          }]
        }]
      })
      .expect(200).then((response) => {
        var ssml = response.body.expected_inputs[0].input_prompt.initial_prompts[0].ssml;
        expect(ssml).to.eql(`What is the name of the artist you would like to hear about?`);
      });
  });

  it('repeats a query', () => {
    return request(action)
      .post('/')
      .send({
        inputs: [{
          intent: 'assistant.intent.action.TEXT',
          raw_inputs: [{
            input_type: 2,
            query: "hello world"
          }],
          arguments: []
        }]
      })
      .expect(200).then((response) => {
        expect(response.body.expect_user_response).to.equal(false);
        var ssml = response.body.final_response.speech_response.text_to_speech;
        expect(ssml).to.equal('You said hello world.');
      });
  });
});

Find the complete source code of the Artsy action on Github.

Local Simulator

GAS does a good job at enabling running of a local development version.

  • Run ngrok to proxy requests to the outside world.
  • Run npm run server to start the local instance.
  • Deploy a preview version of the app with npm run action:autopreview.
  • Run npm run action:simulate for a local simulator, use the web version or even a Google Home device registered under your development account.

Certification

Fulfillment Endpoint

The certification process requires that you deploy the node.js application into a production environment, first. We deployed ours into Google Cloud, which involved creating a vm.yaml, creating a new deployment with gcloud deployment-manager deployments create production --config vm.yaml, transpiling JavaScript with npm run build and deploying the app with gcloud app deploy. You can just push the app to Heroku or AWS Lambda as well.

Google Actions API Project

Create a Google Actions API project from the API Dashboard and configure the project in "Directory Listing". It's important to get the sample invocations right, eg. "Ok Google, ask my first action to ...".

Deploy Action

Deploying the action makes it usable by others by submitting it for approval with Google. Once submitted you have to wait for your action to be rejected or approved, there's no way to un-submit an action without contacting support.

Change the httpExecution URL(s) in action.json to the deployed fulfillment URL, eg. https://my-first-action.appspot-preview.com, ensure the correct action ID is used in package.json, and run npm run action:deploy, which will register and deploy your action.

The application will appear under "Deployment History" in the API dashboard.

Help

I found that the best place to ask questions was stack overflow#actions-on-google and their unusually responsive support team.

Code

Find the complete source code on Github.

Categories: google actions, google assistant, google home


Comments