dialogflow-fulfillment
Version:
Dialogflow Fulfillment Library for Node.js
521 lines (476 loc) • 18.4 kB
JavaScript
/**
* Copyright 2017 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.
*/
const debug = require('debug')('dialogflow:debug');
// Configure logging for hosting platforms agent only support console.log and console.error
debug.log = console.log.bind(console);
// Response Builder classes
const {
V1_TO_V2_PLATFORM_NAME,
PLATFORMS,
} = require('./rich-responses/rich-response');
const Text = require('./rich-responses/text-response');
const Card = require('./rich-responses/card-response');
const Image = require('./rich-responses/image-response');
const Suggestion = require('./rich-responses/suggestions-response');
const PayloadResponse = require('./rich-responses/payload-response');
// Contexts class
const Contexts = require('./contexts');
/**
* Class representing a v2 Dialogflow agent
*/
class V2Agent {
/**
* Constructor for V2Agent object
* To be used in with WebhookClient class
*
* @param {Object} agent instance of WebhookClient class
*/
constructor(agent) {
this.agent = agent;
return this;
}
/**
* Process a v2 Dialogflow webhook request to set class varibles
* for action, parameters, contexts, request source and orignal user query
*
* @private
*/
processRequest_() {
/**
* Dialogflow intent or null if no value
* https://dialogflow.com/docs/intents
* @type {string}
*/
this.agent.intent = this.agent.request_.body.queryResult.intent.displayName;
debug(`Intent: ${this.agent.intent}`);
/**
* Dialogflow action or null if no value: https://dialogflow.com/docs/actions-and-parameters
* @type {string}
*/
this.agent.action = this.agent.request_.body.queryResult.action
? this.agent.request_.body.queryResult.action
: null;
debug(`Action: ${this.agent.action}`);
/**
* Dialogflow input contexts included in the request or null if no value
* https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/WebhookRequest#FIELDS.session
* @type {string}
*/
this.agent.session = this.agent.request_.body.session;
debug(`v2 Session: ${JSON.stringify(this.agent.session)}`);
/**
* Dialogflow parameters included in the request or null if no value
* https://dialogflow.com/docs/actions-and-parameters
* @type {Object[]}
*/
this.agent.parameters =
this.agent.request_.body.queryResult.parameters || {}; // https://dialogflow.com/docs/actions-and-parameters
debug(`Parameters: ${JSON.stringify(this.agent.parameters)}`);
/**
* Dialogflow input contexts included in the request or null if no value
* convert v2 contexts to v1 contexts
* https://dialogflow.com/docs/contexts
* @type {string}
*/
if (this.agent.request_.body.queryResult.outputContexts) {
this.agent.contexts = this.agent.request_.body.queryResult.outputContexts
.map((context) => this.convertV2ContextToV1Context_(context));
} else {
this.agent.contexts = [];
}
debug(`Request contexts: ${JSON.stringify(this.agent.contexts)}`);
/**
* Instance of Dialogflow contexts class to provide an API to set/get/delete contexts
*
* @type {Contexts}
*/
this.agent.context = new Contexts(
this.agent.request_.body.queryResult.outputContexts,
this.agent.session);
/**
* Dialogflow source included in the request or null if no value
* https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/projects.agent.intents#Platform
* @type {string}
*/
const detectIntentRequest = this.agent.request_.body.originalDetectIntentRequest;
if (detectIntentRequest) {
const requestSource =
detectIntentRequest.source ||
(detectIntentRequest.payload && detectIntentRequest.payload.source) ||
null;
this.agent.requestSource = V1_TO_V2_PLATFORM_NAME[requestSource] || requestSource;
}
debug(`Request source: ${JSON.stringify(this.agent.requestSource)}`);
/**
* Dialogflow original request object from detectIntent/query or platform integration
* (Google Assistant, Slack, etc.) in the request or null if no value
* https://dialogflow.com/docs/reference/agent/query#query_parameters_and_json_fields
* @type {object}
*/
this.agent.originalRequest = this.agent.request_.body.originalDetectIntentRequest;
debug(`Original Request: ${JSON.stringify(this.agent.originalRequest)}`);
/**
* Original user query as indicated by Dialogflow or null if no value
* https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/
* projects.agent.sessions/detectIntent#QueryResult.FIELDS.query_text
* @type {string}
*/
this.agent.query = this.agent.request_.body.queryResult.queryText;
debug(`Original query: ${JSON.stringify(this.agent.query)}`);
/**
* Original request language code (i.e. "en")
* @type {string} locale language code indicating the spoken/written language of the original request
*/
this.agent.locale = this.agent.request_.body.queryResult.languageCode;
debug(`Request locale: ${JSON.stringify(this.agent.query)}`);
/**
* List of messages defined in Dialogflow's console for the matched intent
* https://dialogflow.com/docs/rich-messages
*
* @type {RichResponse[]}
*/
if (this.agent.request_.body.queryResult.fulfillmentMessages) {
const consoleMessages = this.agent.request_.body.queryResult.fulfillmentMessages;
this.agent.consoleMessages = this.getConsoleMessages_(consoleMessages);
} else {
this.agent.consoleMessages = [];
}
debug(`Console messages: ${JSON.stringify(this.agent.consoleMessages)}`);
/**
* Alternative query results that have a high match score
* Query results can be from other Dialogflow intents or Knowledge Connectors
* https://cloud.google.com/dialogflow-enterprise/alpha/docs/knowledge-connectors
* Note:this feature is only available in Dialogflow API V2
*
* @type {object}
*/
if (this.agent.request_.body.alternativeQueryResults) {
this.agent.alternativeQueryResults = this.agent.request_.body.alternativeQueryResults;
}
debug(`Alternative query result: ${JSON.stringify(this.agent.alternativeQueryResults)}`);
}
/**
* Add v2 text response to Dialogflow fulfillment webhook request based on
* single, developer defined text response
*
* @private
*/
addTextResponse_() {
const message = this.agent.responseMessages_[0];
const fulfillmentText = message.ssml || message.text;
this.addJson_({fulfillmentText: fulfillmentText});
}
/**
* Add v2 payload response to Dialogflow fulfillment webhook request based
* on developer defined payload response
*
* @param {Object} payload to back to requestSource (i.e. Google, Slack, etc.)
* @param {string} requestSource string indicating the source of the initial request
* @private
*/
addPayloadResponse_(payload, requestSource) {
this.addJson_({payload: payload.getPayload_(requestSource)});
}
/**
* Add v2 response to Dialogflow fulfillment webhook request based on developer
* defined response messages and original request source
*
* @param {string} requestSource string indicating the source of the initial request
* @private
*/
addMessagesResponse_(requestSource) {
let messages = this.buildResponseMessages_(requestSource);
if (messages.length > 0) {
this.addJson_({fulfillmentMessages: messages});
}
}
/**
* Add v2 response to Dialogflow fulfillment webhook request
*
* @param {Object} responseJson JSON to send to Dialogflow
* @private
*/
addJson_(responseJson) {
if (!this.responseJson_) {
this.responseJson_ = {};
}
Object.assign(this.responseJson_, responseJson);
}
/**
* Send v2 response to Dialogflow fulfillment webhook request
*
* @param {string} requestSource string indicating the source of the initial request
* @private
*/
sendResponses_(requestSource) {
let responseJson = this.responseJson_;
if (!responseJson) {
throw new Error(`No responses defined for platform: ${requestSource}`);
}
responseJson.outputContexts = this.agent.context.getV2OutputContextsArray();
if (this.agent.followupEvent_) {
responseJson.followupEventInput = this.agent.followupEvent_;
}
if (this.agent.endConversation_) {
responseJson.triggerEndOfConversation = this.agent.endConversation_;
}
debug('Response to Dialogflow: ' + JSON.stringify(responseJson));
this.agent.response_.json(responseJson);
}
/**
* Builds a list of v2 message objects to send back to Dialogflow based on
* developer defined responses and the request source
*
* @param {string} requestSource string indicating the source of the initial request
* @return {Object[]} message objects
* @private
*/
buildResponseMessages_(requestSource) {
return this.agent.responseMessages_
.map((message) => message.getV2ResponseObject_(requestSource))
.filter((arr) => arr);
}
/**
* Add an v2 outgoing context
*
* @param {object} context an object representing a v1 or v2 outgoing context
* @private
*/
addContext_(context) {
// Check and see if a v2 context object was added
if (context.name.match('/contexts/')) {
context = this.convertV2ContextToV1Context_(context);
}
this.agent.context.set(context);
}
/**
* Convert a v2 context object to a v1 context object
*
* @param {object} v2Context an object representing a v2 context
* @return {object} v1Context an object representing a v1 context
* @private
*/
convertV2ContextToV1Context_(v2Context) {
let v1Context = {};
const v2ContextNamePrefixLength = this.agent.session.length + '/contexts/'.length;
v1Context.name = v2Context.name.slice(v2ContextNamePrefixLength);
v1Context.lifespan = v2Context.lifespanCount;
v1Context.parameters = v2Context.parameters;
return v1Context;
}
/**
* Add an v2 followup event
*
* @param {Object} event an object representing a followup event
* @private
*/
setFollowupEvent_(event) {
if (!event.languageCode) {
event.languageCode = this.agent.locale;
}
this.agent.followupEvent_ = event;
}
/**
* Add a response or list of responses to be sent to Dialogflow and end the conversation
* Note: Only supported on Dialogflow v2's telephony gateway, Google Assistant and Alexa integrations
*
* @param {RichResponse|string|RichResponse[]|string[]} responses (list) or single responses
*/
end_(responses) {
this.agent.endConversation_ = true;
this.agent.add(responses);
}
/**
* Add an v2 Actions on Google response
*
* @param {Object} response a Actions on Google Dialogflow v2 webhook response
* @private
*/
addActionsOnGoogle_(response) {
if (response.outputContexts) {
response.outputContexts.forEach( (context) => {
this.addContext_(context);
});
}
this.agent.add(new PayloadResponse(
PLATFORMS.ACTIONS_ON_GOOGLE,
response.payload.google)
);
}
/**
* Get messages defined in Dialogflow's console for matched intent
*
* @param {Object[]} consoleMessageList is a list of v2 Dialogflow messages from Dialogflow's console
* @return {RichResponse[]} list of RichResponse objects
* @private
*/
getConsoleMessages_(consoleMessageList) {
// Functions to transpose fulfillment messages from Dialogflow's console to fulfillment library classes
const richResponseMapping = {
text: this.convertTextJson_,
card: this.convertCardJson_,
image: this.convertImageJson_,
payload: this.convertPayloadJson_,
quickReplies: this.convertQuickRepliesJson_,
simpleResponses: this.convertSimpleResponsesJson_,
basicCard: this.convertBasicCardJson_,
suggestions: this.convertSuggestionsJson_,
};
let richConsoleMessages = []; // list of messages to be returned
// iterate through each message recived in the webhook request
consoleMessageList.forEach( (consoleMessageJson) => {
const richMessageType = Object.keys(consoleMessageJson).find((key) => key !== 'platform');
if (richResponseMapping[richMessageType]) {
const messagePlatform = consoleMessageJson.platform ? consoleMessageJson.platform : undefined;
// convert the JSON to fufillment classes
let richResponse = richResponseMapping[richMessageType](consoleMessageJson, messagePlatform);
richResponse ? richConsoleMessages = richConsoleMessages.concat(richResponse): null;
} else {
debug(`Unsupported console message type "${richMessageType}"`);
}
});
return richConsoleMessages;
};
/**
* Convert incoming text message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertTextJson_(messageJson, platform) {
if (!messageJson.text.text[0]) return null;
else return new Text({text: messageJson.text.text[0], platform: platform});
}
/**
* Convert incoming card message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertCardJson_(messageJson, platform) {
return new Card({
title: messageJson.card.title || ' ',
text: messageJson.card.subtitle,
imageUrl: messageJson.card.imageUri,
buttonText: messageJson.card.buttons ? messageJson.card.buttons[0].text: null,
buttonUrl: messageJson.card.buttons ? messageJson.card.buttons[0].postback: null,
platform: platform,
});
}
/**
* Convert incoming image message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertImageJson_(messageJson, platform) {
return new Image({
imageUrl: messageJson.image.imageUri,
platform: platform,
});
}
/**
* Convert incoming payload message object JSON into a Payload rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertPayloadJson_(messageJson, platform) {
return new PayloadResponse(platform, messageJson.payload, {
rawPayload: true,
sendAsMessage: true,
});
}
/**
* Convert incoming quick reply message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertQuickRepliesJson_(messageJson, platform) {
if (!messageJson.suggestions) return null;
let suggestions = [];
messageJson.suggestions.forEach( (consoleMessageJson, iterator) => {
suggestions.push(new Suggestion({
title: messageJson.quickReplies.quickReplies[iterator],
platform: platform,
}));
});
return suggestions;
}
/**
* Convert incoming simple response message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertSimpleResponsesJson_(messageJson, platform) {
return new Text({
text: messageJson.simpleResponses.simpleResponses[0].textToSpeech,
platform: platform,
});
}
/**
* Convert incoming basic card message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertBasicCardJson_(messageJson, platform) {
return new Card({
title: messageJson.basicCard.title || ' ',
text: messageJson.basicCard.formattedText,
imageUrl: messageJson.basicCard.image ? messageJson.basicCard.image.imageUri : null,
buttonText: messageJson.basicCard.buttons ? messageJson.basicCard.buttons[0].title : null,
buttonUrl: messageJson.basicCard.buttons ? messageJson.basicCard.buttons[0].openUriAction.uri : null,
platform: platform,
});
}
/**
* Convert incoming suggestions message object JSON into a Text rich response
*
* @param {Object} messageJson is a the JSON implementation of the message
* @param {string} platform is the platform of the message object
* @return {RichResponse} richResponse implementation of the message
* @private
*/
convertSuggestionsJson_(messageJson, platform) {
if (!messageJson.suggestions) return null;
let suggestions = [];
messageJson.suggestions.suggestions.forEach( (consoleMessageJson, iterator) => {
suggestions.push(new Suggestion({
title: messageJson.suggestions.suggestions[iterator].title,
platform: platform,
}));
});
return suggestions;
}
}
module.exports = V2Agent;