Actions on Google Node.js Client Library Version 1 Migration Guide

Overview

This documentation covers migrating to the Actions on Google Node.js Client Library version 2 (v2), which we released on April 16th, 2018 and announced in this blog post.

Besides additional features, client library v2 aims to improve the developer experience with more idiomatic JavaScript and a more extensible library architecture. Part of the simplification is a result of dropping legacy support for Actions SDK Conversation Webhook API v1, which will no longer be supported after May 17, 2018.

New frameworks system

In v1 of the client library, working with frameworks other than Express required you to fork the client library and modify the code. For example, this snippet shows how you previously integrated Cloud Functions for Firebase in v1:

// v1
const functions = require('firebase-functions');
const { DialogflowApp } = require('actions-on-google');

exports.factsAboutGoogle = functions.https.onRequest((req, res) => {
  const app = new DialogflowApp({ request: req, response: res });
  // fulfillment code here
});

In v2, we added a framework system that allows for easy integration of the library in any Node.js web framework. A web framework like Express, Lambda, or Koa can be represented in the library as a class that can handle requests coming from a specific server framework. The v2 client library will auto-detect the framework and handle it seamlessly.

If you are an existing Firebase Function developer, you should simply use app as the function handler for functions.https.onRequest(). The snippet below shows how to integrate Cloud Functions for Firebase using the v2 framework support:

// v2
const functions = require('firebase-functions');
const { dialogflow } = require('actions-on-google');

const app = dialogflow();

// fulfillment code here

exports.factsAboutGoogle = functions.https.onRequest(app);

In addition to Cloud Functions for Firebase, v2 also supports other frameworks, including AWS Lambda and self-hosted frameworks.

The snippet below shows an example of AWS Lambda framework support in v2:

// v2
const { dialogflow } = require('actions-on-google');

const app = dialogflow();

// fulfillment code here

exports.factsAboutGoogle = app;

The snippet below shows an example of self-hosted express framework support in v2:

// v2
const express = require('express');
const bodyParser = require('body-parser');
const { dialogflow } = require('actions-on-google');

const app = dialogflow();

// fulfillment code here

express().use(bodyParser.json(), app).listen(3000);

App instance API changes

In v2, we've split the app object into a global app instance (that represents the Action itself) and a per conversation conv instance. This allows you to set cross-conversational information, like a debug flag or a default data initiation function, in the app instance, and clearly distinguish per conversation information in the conv instance.

In addition, v2 provides helpers to perform matching for Dialogflow intents and Actions SDK intents on the global app instance. This contrasts with the v1 actionMap, which maps to Dialogflow action names instead of intent names.

The following snippet shows an example of the single app instance in v1:

// v1
const functions = require('firebase-functions');
const { DialogflowApp } = require('actions-on-google');

const actionMap = new Map();

actionMap.set('input.welcome', app => {
  app.ask('How are you?');
});

exports.factsAboutGoogle = functions.https.onRequest((req, res) => {
  const app = new DialogflowApp({ request: req, response: res });
  app.handleRequest(actionMap);
});

The following snippet shows an example of an app instance and conv instance in v2:

// v2
const functions = require('firebase-functions');
const { dialogflow } = require('actions-on-google');

const app = dialogflow();

app.intent('Default Welcome Intent', conv => {
  conv.ask('How are you?');
});

exports.factsAboutGoogle = functions.https.onRequest(app);

Ask/Close (not Ask/Tell) and more idiomatic JavaScript

In v2, we've renamed tell to conv.close(). This rename is meant to clarify that you should call this method when you want the conversation to end (this is also referred to as "closing the mic").

Example snippet with v1:

// v1
actionMap.set('input.welcome', app => {
  app.tell('Thanks for talking to me!'); // not clear what tell means
});

Example snippet with v2:

// v2
app.intent('Default Welcome Intent', conv => {
  conv.close('Thanks for talking to me!'); // explicit mic close
});

In v2, ask is now the primary method to send a response back to the user. We now explicitly encourage usage of classes to track type information. This means that you can call ask multiple times and let the library construct responses on your behalf.

Example buildRichResponse snippet with v1:

// v1
actionMap.set('input.welcome', app => {
  const color = app.getArgument('color');
  const num = app.getArgument('num');
  app.ask(app.buildRichResponse()
    .addSimpleResponse(`Dialogflow likes ${color}`)
    .addSuggestions(['Ok', 'Cool'])
    .addBasicCard(app.buildBasicCard()
      .setTitle('Card Title')
      .setImage('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png', 'Google Logo')
      .addButton('Button Title', 'https://www.google.com')
      .setImageDisplay(app.ImageDisplays.WHITE)
    )
  );
});

Example showing how you can call ask multiple times with v2:

// v2
app.intent('Default Welcome Intent', (conv, { color, num }) => {
  conv.ask(`Dialogflow likes ${color}`);
  conv.ask(new Suggestions('Ok', 'Cool'), new BasicCard({
    title: 'Card Title',
    image: { // Mostly, you can provide just the raw API objects
      url: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png',
      accessibilityText: 'Google Logo',
    },
    buttons: new Button({ // Wrapper for complex sub Objects
      title: 'Button Title',
      url: 'https://www.google.com',
    }),
    display: 'WHITE',
  }));
});

By using classes as the primary response construction technique, there are now no builders. Instead, you can use anonymous objects with named properties to construct responses.

Example showing how you previously used builder with v1:

// v1
app.buildRichResponse(
  app.buildBasicCard('some string')
    .setImage('https://site.com/img.png', 'some other string'));

Example showing how to use v2 classes via an anonymous object with named properties:

// v2
conv.ask(new BasicCard({
  title: 'some string',
  image: new Image({
    url: 'https://site.com/img.png',
    alt: 'some other string',
  }),
}));

Modularized request parsers and string enums

In v2, parsing is separated out into different classes to handle a certain type of request input. This results in a more modularized API, more concise naming, and structure closer to the request API data. In addition, enums are just strings documented in JavaScript using typedefs and enforced in TypeScript with string union types for enums.

Example of single namespace methods and appended enums with v1:

// v1
const capability = app.SurfaceCapabilities.SCREEN_OUTPUT;
app.hasAvailableSurfaceCapabilities(capability)

Example of modularized parsing and string enums with v2:

// v2
const capability = 'actions.capability.SCREEN_OUTPUT';
conv.available.surfaces.capabilities.has(capability)

Clear separation of services

In v2, we introduce the concept of "services," which represent a separate webhook fulfillment response and request format. Dialogflow and Actions SDK are examples of conversation-based services that v2 supports.

This approach reduces ambiguity around shared methods, as shown by the following examples:

Usage V1 DialogflowApp V2 DialogflowConversation
Gets the Dialogflow action app.getIntent() conv.action
Gets the Dialogflow intent n/a conv.intent
Gets a Dialogflow parameter app.getArgument(param) conv.parameters[param]
Gets a Dialogflow context parameter app.getContextArgument(context, param) conv.contexts.get(context).parameters[param]

Intent handler helpers and arguments

In v1, there was a separate askFor intent helper and get${VALUE} helper created for every intent. In v2, we've abstracted the askFor intent helpers into classes with anonymous objects that have named properties as arguments.

For the getters, we now more closely take from the API to make it clear what the code is doing.

In addition, we included a handy shortcut for retrieving single argument intents, as well as a shortcut for retrieving parameters for Dialogflow and the raw text input for Actions SDK.

Example of using the askFor helper and get helper with v1:

// v1
const app = new DialogflowApp({request: req, response: res});

const actionMap = new Map();

actionMap.set('input.welcome', app => {
  const permission = app.SupportedPermissions.NAME;
  app.askForPermission('To know who you are', permission);
});

actionMap.set('get.name', app => {
  if (app.isPermissionGranted()) { // how does this get the argument?
    app.tell(`Your name is ${app.getUserName().displayName}`);
  } else {
    app.tell('Sorry, I could not get your name.');
  }
});

app.handleRequest(actionMap);

Example of using the ask permission helper for Dialogflow with v2:

// v2
const app = dialogflow();

app.intent('Default Welcome Intent', conv => {
  conv.ask(new Permission({
    context: 'To know who you are',
    permissions: 'NAME',
  }));
});

// Create a Dialogflow intent with the `actions_intent_PERMISSION` event
app.intent('Get Name', (conv, params, granted) => {
  // params are the Dialogflow parameters as a key value map
  // granted is the inferred first (and only) argument value, boolean true if granted, false if not
  const explicit = conv.arguments.get('PERMISSION'); // also retrievable with explicit arguments.get
  const name = conv.user.name;
});

Example of using the ask place helper for Actions SDK with v2, which requires separate status parsing:

// v2
const app = actionssdk();

app.intent('actions.intent.MAIN', conv => {
  conv.ask(new Place({
    prompt: 'Where do you want to get picked up?',
    context: 'To find a place to pick you up',
  }));
});

app.intent('actions.intent.PLACE', (conv, input, place, status) => {
  // input is the raw input text
  if (place) {
    conv.close(`Ah, I see. You want to get picked up at ${place.formattedAddress}`);
  } else {
    // Possibly do something with status
    conv.close(`Sorry, I couldn't find where you want to get picked up`);
  }
});

Additionally, async tasks now have built-in direct support in the library. To perform an async task, you must return a Promise to the intent handler.

This allows the library to track the intent handler correctly. If you intend for a Promise to be run every time, the Promise cannot be set once in the global scope as modern serverless frameworks will cache globals for performance purposes.

Example of an async task with v2:

// v2
const app = dialogflow();

const asyncTask = () => new Promise(
  resolve => setTimeout(resolve, 1000)
);

app.intent('Default Welcome Intent', conv => {
  return asyncTask()
    .then(() => conv.ask('I took one second to run'));
});

If your runtime supports async await (which Firebase Functions does not support while it's still on Node.js 6), then you can further simplify the call.

Example of an async await task with v2:

// v2
const app = dialogflow();

const asyncTask = () => new Promise(
  resolve => setTimeout(resolve, 1000)
);

app.intent('Default Welcome Intent', async conv => {
  await asyncTask();
  conv.ask('I took one second to run');
});

Scaling with plugins and middleware

We've designed v2 to be extensible via plugins and middleware. In v2, a plugin is a packaged way to incorporate third-party shared code from the community. You can use the v2 plugin interface to integrate with other third-party plugins or with your own middleware.

You can create your own conversation services middleware to integrate with Dialogflow and Actions SDK. The middleware layer consists of a function you define that the client library automatically runs before the IntentHandler. Using a middleware layer lets you modify the Conversation instance and add additional functionality. To share your middleware with the community, you can also package your middleware into a plugin.

You can use a plugin by importing it via the app.use method. This allows a plugin to modify the app instance and, thus, modify the library's functionality.

The following example shows how you might use a plugin with v2:

// v2
const { dialogflow } = require('actions-on-google');
const { randomize, Randomization } = require('randomize');

const app = dialogflow()
  .use(randomize);

app.intent('Tell Greeting', conv => {
  conv.ask(`The last thing I told you was ${conv.randomize.last}`)
  conv.ask(new Randomization(
    'How are you?',
    'Are you having a good day?',
  ));
});

Plugins aren't just for sharing code with other people; you can also use them to structure your codebase into separate areas of concern. This approach is called creating an internal plugin.

The following example shows how you might use an internal plugin with v2 to structure your own code. It organizes all the intents into two sets and represent the sets as plugins:

// v2
const { dialogflow } = require('actions-on-google');

const intentSet1 = app => {
  app.intent('intentSet1A', handler1A);
  app.intent('intentSet1B', handler1B);
};

const intentSet2 = app => {
  app.intent('intentSet2A', handler2A);
});

const app = dialogflow()
  .use(intentSet1)
  .use(intentSet2)

You can also use v2 plugins to add more methods on the app instance. The following snippet shows an example of using v2 internal plugins to add functionality to app. In this case, it creates a new method on app called setIntentPair, which programmatically sets two intent handlers with intents that are related:

// v2
const { dialogflow } = require('actions-on-google');

const extraFunctionality = app => {
  app.setIntentPair = (base, handler1, handler2) => {
    app.intent(`${base}1`, handler1);
    app.intent(`${base}2`, handler2);
  };
};

const app = dialogflow()
  .use(extraFunctionality);

app.setIntentPair('something', conv => {
  conv.ask('intent something1 triggered');
}, conv => {
  conv.ask('intent something2 triggered');
});

Additionally, conversation services like Dialogflow and Actions SDK expose an app.middleware method that allows you to add properties or helper classes to the conv instance.

The following snippet shows an example of using v2 for internal middleware:

// v2
const { dialogflow } = require('actions-on-google');

class Helper {
  constructor(conv) {
    this.conv = conv;
  }

  func1() {
    this.conv.ask(`What's up?`);
  }
}

const app = dialogflow()
  .middleware(conv => {
    conv.helper = new Helper(conv);
  });

app.intent('Default Welcome Intent', conv => {
  conv.helper.func1();
});

TypeScript

The v1 library had unofficial, community-contributed types that you had to install separately, as shown in the following command snippet:

# v1
npm install actions-on-google@^1.0.0
npm install --dev @types/actions-on-google

In v2, the client library is built in TypeScript. The TypeScript declarations are included by default with the library, so you only need to install the client library, as shown in the following command snippet:

# v2
npm install actions-on-google@^2.0.0