actions-on-google
Version:
Actions on Google Client Library for Node.js
462 lines (426 loc) • 12.8 kB
text/typescript
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Api from './api/v2';
import * as ApiV1 from './api/v1';
import * as ActionsApi from '../actionssdk/api/v2';
import {
Conversation,
ConversationBaseOptions,
ConversationOptionsInit,
} from '../actionssdk';
import {ProtoAny, JsonObject} from '../../common';
import {Contexts, ContextValues, Parameters} from './context';
import {Incoming} from './incoming';
const CONV_DATA_CONTEXT = '_actions_on_google';
const CONV_DATA_CONTEXT_LIFESPAN = 99;
const SIMULATOR_WARNING =
'Cannot display response in Dialogflow simulator. ' +
'Please test on the Google Assistant simulator instead.';
/** @hidden */
export interface SystemIntent {
intent: string;
data: ProtoAny<string, JsonObject>;
}
/** @hidden */
export interface GoogleAssistantResponse {
expectUserResponse?: boolean;
noInputPrompts?: ActionsApi.GoogleActionsV2SimpleResponse[];
isSsml?: boolean;
richResponse?: ActionsApi.GoogleActionsV2RichResponse;
systemIntent?: SystemIntent;
userStorage?: string;
speechBiasingHints?: string[];
}
/** @hidden */
export interface PayloadGoogle {
google: GoogleAssistantResponse;
}
/** @public */
export interface DialogflowConversationOptions<TConvData, TUserStorage>
extends ConversationBaseOptions<TConvData, TUserStorage> {
/** @public */
body?:
| Api.GoogleCloudDialogflowV2WebhookRequest
| ApiV1.DialogflowV1WebhookRequest;
}
const isV1 = (
body:
| Api.GoogleCloudDialogflowV2WebhookRequest
| ApiV1.DialogflowV1WebhookRequest
): body is ApiV1.DialogflowV1WebhookRequest =>
!!(body as ApiV1.DialogflowV1WebhookRequest).result;
const isSimulator = (
body:
| Api.GoogleCloudDialogflowV2WebhookRequest
| ApiV1.DialogflowV1WebhookRequest
) => {
if (isV1(body)) {
return !body.originalRequest;
}
if (!body.originalDetectIntentRequest) {
return false;
}
return (
Object.keys(body.originalDetectIntentRequest.payload!).length === 0 &&
!!body.responseId
);
};
const getRequest = (
body:
| Api.GoogleCloudDialogflowV2WebhookRequest
| ApiV1.DialogflowV1WebhookRequest
): ActionsApi.GoogleActionsV2AppRequest => {
if (isV1(body)) {
const {originalRequest = {}} = body;
const {data = {}} = originalRequest;
return data;
}
const {originalDetectIntentRequest = {}} = body;
const {payload = {}} = originalDetectIntentRequest;
return payload;
};
const serializeData = <TConvData>(data: TConvData) => JSON.stringify(data);
const deserializeData = <TContexts extends Contexts, TConvData>(
contexts: ContextValues<TContexts>,
defaultData?: TConvData
): TConvData => {
const context = contexts.get(CONV_DATA_CONTEXT);
if (context) {
const {data} = context.parameters;
if (typeof data === 'string') {
return JSON.parse(data);
}
}
return Object.assign({}, defaultData) as TConvData;
};
/** @public */
export class DialogflowConversation<
TConvData = JsonObject,
TUserStorage = JsonObject,
TContexts extends Contexts = Contexts
> extends Conversation<TUserStorage> {
/** @public */
body:
| Api.GoogleCloudDialogflowV2WebhookRequest
| ApiV1.DialogflowV1WebhookRequest;
/**
* Get the current Dialogflow action name.
*
* @example
* ```javascript
*
* app.intent('Default Welcome Intent', conv => {
* const action = conv.action
* })
* ```
*
* @public
*/
action: string;
/**
* Get the current Dialogflow intent name.
*
* @example
* ```javascript
*
* app.intent('Default Welcome Intent', conv => {
* const intent = conv.intent // will be 'Default Welcome Intent'
* })
* ```
*
* @public
*/
intent: string;
/**
* The Dialogflow parameters from the current intent.
* Values will only be a string, an Object, or undefined if not included.
*
* Will also be sent via intent handler 3rd argument which is the encouraged method to retrieve.
*
* @example
* ```javascript
*
* // Encouraged method through intent handler
* app.intent('Tell Greeting', (conv, params) => {
* const color = params.color
* const num = params.num
* })
*
* // Encouraged method through destructuring in intent handler
* app.intent('Tell Greeting', (conv, { color, num }) => {
* // now use color and num as variables
* }))
*
* // Using conv.parameters
* app.intent('Tell Greeting', conv => {
* const parameters = conv.parameters
* // or destructed
* const { color, num } = conv.parameters
* })
* ```
*
* @public
*/
parameters: Parameters;
/** @public */
contexts: ContextValues<TContexts>;
/** @public */
incoming: Incoming;
/**
* The user's raw input query.
*
* @example
* ```javascript
*
* app.intent('User Input', conv => {
* conv.close(`You said ${conv.query}`)
* })
* ```
*
* @public
*/
query: string;
/**
* The session data in JSON format.
* Stored using contexts.
*
* @example
* ```javascript
*
* app.intent('Default Welcome Intent', conv => {
* conv.data.someProperty = 'someValue'
* })
* ```
*
* @public
*/
data: TConvData;
/** @public */
version: number;
/** @hidden */
_followup?:
| Api.GoogleCloudDialogflowV2EventInput
| ApiV1.DialogflowV1FollowupEvent;
/** @hidden */
_init: ConversationOptionsInit<TConvData, TUserStorage>;
/** @public */
constructor(
options: DialogflowConversationOptions<TConvData, TUserStorage> = {}
) {
const {body = {}} = options;
super({
request: getRequest(body),
headers: options.headers,
init: options.init,
ordersv3: options.ordersv3,
});
this.body = body;
if (isV1(this.body)) {
this.version = 1;
const {result = {}} = this.body;
const {
action = '',
parameters = {},
contexts,
resolvedQuery = '',
metadata = {},
fulfillment,
} = result;
const {intentName = ''} = metadata;
this.action = action;
this.intent = intentName;
this.parameters = parameters;
this.contexts = new ContextValues(contexts);
this.incoming = new Incoming(fulfillment);
this.query = resolvedQuery;
} else {
this.version = 2;
const {queryResult = {}} = this.body;
const {
action = '',
parameters = {},
outputContexts,
intent = {},
queryText = '',
fulfillmentMessages,
} = queryResult;
const {displayName = ''} = intent;
this.action = action;
this.intent = displayName;
this.parameters = parameters;
this.contexts = new ContextValues(outputContexts, this.body.session);
this.incoming = new Incoming(fulfillmentMessages);
this.query = queryText;
}
for (const key in this.parameters) {
const value = this.parameters[key];
if (typeof value !== 'object') {
// Convert all non-objects to strings for consistency
this.parameters[key] = String(value);
}
}
this.data = deserializeData(this.contexts, this._init.data);
}
/**
* Triggers an intent of your choosing by sending a followup event from the webhook.
* Final response can theoretically include responses but these will not be handled
* by Dialogflow. Dialogflow will not pass anything back to Google Assistant, therefore
* Google Assistant specific information, most notably conv.user.storage, is ignored.
*
* @example
* ```javascript
*
* const app = dialogflow()
*
* // Create a Dialogflow intent with event 'apply-for-license-event'
*
* app.intent('Default Welcome Intent', conv => {
* conv.followup('apply-for-license-event', {
* date: new Date().toISOString(),
* })
* // The dialogflow intent with the 'apply-for-license-event' event
* // will be triggered with the given parameters `date`
* })
* ```
*
* @param event Name of the event
* @param parameters Parameters to send with the event
* @param lang The language of this query.
* See {@link https://dialogflow.com/docs/languages|Language Support}
* for a list of the currently supported language codes.
* Note that queries in the same session do not necessarily need to specify the same language.
* By default, it is the languageCode sent with Dialogflow's queryResult.languageCode
* @public
*/
followup(event: string, parameters?: Parameters, lang?: string) {
this._responded = true;
if (this.version === 1) {
this._followup = {
name: event,
data: parameters,
};
return this;
}
const body = this.body as Api.GoogleCloudDialogflowV2WebhookRequest;
this._followup = {
name: event,
parameters,
languageCode: lang || body.queryResult!.languageCode,
};
return this;
}
/** @public */
serialize():
| Api.GoogleCloudDialogflowV2WebhookResponse
| ApiV1.DialogflowV1WebhookResponse {
if (this._raw) {
return this._raw;
}
let payload: PayloadGoogle | undefined;
if (this._followup) {
this.digested = true;
} else {
const {
richResponse,
expectUserResponse,
userStorage,
expectedIntent,
noInputPrompts,
speechBiasingHints,
} = this.response();
const google: GoogleAssistantResponse = {
expectUserResponse,
systemIntent: expectedIntent && {
intent: expectedIntent.intent!,
data: expectedIntent.inputValueData as ProtoAny<string, JsonObject>,
},
noInputPrompts,
speechBiasingHints,
};
if (richResponse.items!.length) {
google.richResponse = richResponse;
}
if (userStorage) {
google.userStorage = userStorage;
}
payload = {google};
}
const convDataDefault = deserializeData<TContexts, TConvData>(
this.contexts,
this._init.data
);
const convDataIn = serializeData(convDataDefault);
const convDataOut = serializeData(this.data);
if (convDataOut !== convDataIn) {
// Previously was setting every webhook call
// But now will only set if different so lifespan does not get reset
this.contexts.set(CONV_DATA_CONTEXT, CONV_DATA_CONTEXT_LIFESPAN, {
data: convDataOut,
});
}
const simulator = isSimulator(this.body);
if (this.version === 1) {
const response: ApiV1.DialogflowV1WebhookResponse = {
data: payload,
followupEvent: this._followup,
};
const contextOut = this.contexts._serializeV1();
if (contextOut.length) {
response.contextOut = contextOut;
}
if (simulator && payload) {
const {richResponse = {}} = payload.google;
const {items = []} = richResponse;
// Simulator only shows speech response
// Since this is only shown to the simulator as text, the speech is the displayText
response.speech = SIMULATOR_WARNING;
if (!payload.google.systemIntent && items.length < 2) {
for (const {simpleResponse} of items) {
if (simpleResponse) {
response.speech =
simpleResponse.displayText || simpleResponse.textToSpeech;
break;
}
}
}
}
return response;
}
const response: Api.GoogleCloudDialogflowV2WebhookResponse = {
payload,
followupEventInput: this._followup,
};
const outputContexts = this.contexts._serialize();
if (outputContexts.length) {
response.outputContexts = outputContexts;
}
if (simulator && payload) {
const {richResponse = {}} = payload.google;
const {items = []} = richResponse;
response.fulfillmentText = SIMULATOR_WARNING;
if (!payload.google.systemIntent && items.length < 2) {
for (const {simpleResponse} of items) {
if (simpleResponse) {
response.fulfillmentText =
simpleResponse.displayText || simpleResponse.textToSpeech;
break;
}
}
}
}
return response;
}
}