Dialogflow +外部API + Google Cloud Functions *无* Firebase:如何返回履行响应?

时间:2019-03-11 16:03:41

标签: javascript node.js google-cloud-functions dialogflow dialogflow-fulfillment

我正在尝试构建一个Dialogflow聊天机器人,该机器人可通过Google Cloud Functions从外部API获取数据,但使用Firebase 无需。尽管进行了广泛的搜索,但我还没有找到任何很好的例子或模板。似乎所有可用的示例都使用Firebase函数。

我是一名新手程序员,不熟悉Node.js,Promises和所有其他花哨的东西,但我收集到即使没有Firebase,也可以通过Dialogflow访问外部API(我使用的是Google的付费版本云)。

我尝试使用此Dialogflow天气API示例创建我的Google Cloud Function,即使我也使用Firebase,这也是我所需要的最接近的东西:  https://github.com/dialogflow/fulfillment-weather-nodejs/blob/master/functions/index.js#L72

问题是我的代码在“ res.on('end'...”行附近的某个地方失败了,我无法弄清楚原因。GoogleCloud Stackdriver日志仅给出了相当无用的消息“忽略来自完成的功能”,但没有告诉我异常是什么。

这是我的index.js代码的编辑版本:

'use strict';

const rpn = require('request-promise-native'); 
const http = require('http');
const hostAPI = 'my host API URL goes here';
const url = require('url');
const {WebhookClient} = require('dialogflow-fulfillment');

exports.myGoogleCloudSearch = (req, res) => {
  	const agent = new WebhookClient({request: req, response: res}); // Dialogflow agent
	// These are logged in Google Cloud Functions
	console.log('Dialogflow Request headers: ' + JSON.stringify(req.headers));
  	console.log('Dialogflow Request body: ' + JSON.stringify(req.body));
  
  	// Default welcome intent, this comes through to Dialogflow
  	function welcome(agent) {
    	agent.add('This welcome message comes from Google Cloud Functions.');
    }

  	// Default fallback intent, this also comes through
  	function fallback(agent) {
    	agent.add('This is the fallback response from Google Cloud Functions.');
    }
		
 	function searchMyInfo(agent) {
    	// get parameters given by user in Dialogflow
      	const param1 = agent.parameters.param1;
      	const param2 = agent.parameters.param2;
      	const param3 = agent.parameters.param3
		// this is logged
      	console.log('Parameters fetched from Dialogflow: ' + param1 + ', ' + param2 + ', ' + param3);
      	
       	var myUrl = hostAPI + param1 + param2 + param3;
		// the URL is correct and also logged
       	console.log('The URL is ' + myUrl);
		
		// Everything up to here has happened between Dialogflow and Google Cloud Functions
		// and inside GCF, and up to here it works
		
		// Next, contact the host API to get the requested information via myUrl
		// Using this as an example but *without* Firebase:
		// https://github.com/dialogflow/fulfillment-weather-nodejs/blob/master/functions/index.js#L41
		
		function getMyInfo(param1, param2, param3) {
			console.log('Inside getMyInfo before Promise'); // this is logged
			return new Promise((resolve, reject) => {
				console.log('Inside getMyInfo after Promise'); // this is logged
				console.log('Should get JSON from ' + myUrl);
				rpn.get(myUrl, (res) => {
					// The code is run at least up to here, since this is logged:
					console.log('Inside rpn.get');
					
					// But then the Google Cloud log just says 
					// "Ignoring exception from a finished function"
					// and nothing below is logged (or run?)
					
					let body = ''; // variable to store response chunks
					res.on('data', (chunk) => {body += chunk;}); // store each response chunk
					res.on('end', () => {
						// this is not logged, so something must go wrong here
						console.log('Inside res.on end block');
						
						// Parse the JSON for desired data
						var myArray = JSON.parse(body); // fetched JSON parsed into array
						console.log(myArray); // not logged
						
						// Here I have more parsing and filtering of the fetched JSON
						// to obtain my desired data. This JS works fine for my host API and returns
						// the correct data if I just run it in a separate html file,
						// so I've left it out of this example because the problem seems
						// to be with the Promise(?).
						
						// Create the output from the parsed data
						// to be passed on to the Dialogflow agent

						let output = agent.add('Parsed data goes here');
						console.log(output);
						resolve(output); // resolve the promise
					}); // res.on end block end
					
					// In case of error
					res.on('error', (error) => {
						// this is not logged either
						console.log('Error calling the host API');
						reject();
					}); // res.on error end
				}); // rpn.get end 
			}); // Promise end
		} // getMyInfo end
		
		// call the host API: this does not seem to work since nothing is logged
		// and no error message is returned

		getMyInfo(param1, param2, param3).then((output) => {
			console.log('getMyInfo call started');
			// Return the results of the getMyInfo function to Dialogflow
			res.json({'fulfillmentText': output});
		}).catch(() => {
			// no error message is given either
			res.json({'fulfillmentText' : 'There was an error in getting the information'});
			console.log('getMyInfo call failed');
		});
		
    } // searchMyInfo(agent) end
  
  	// Mapping functions to Dialogflow intents
  	let intentMap = new Map();
  	intentMap.set('Default Welcome Intent', welcome); // this works
  	intentMap.set('Default Fallback Intent', fallback); // this works
  	intentMap.set('my.search', searchMyInfo); // this does not work
  	agent.handleRequest(intentMap);

}; // exports end

所以我的问题是:如何使这段代码起作用,以将实现响应返回给Dialogflow?默认的欢迎和后备响应确实来自Google Cloud Functions,但是我的自定义意图Webhook响应却没有(即使在Dialogflow中为my.search设置了“启用Webhook调用”)。

3 个答案:

答案 0 :(得分:1)

我确实遇到了同样的问题,正如您所说,我认为这与JavaScript的承诺和异步行为有关。因为当您调用云函数时,该函数先执行然后响应,但是该函数不会等待外部API调用。

我尝试了request客户端,但是当我看到日志视图时,外部api响应在云函数响应之后出现。

所以我选择使用axios(node.js的基于Promise的HTTP客户端),然后云功能起作用。

这是Dialogflow +外部API + Google Cloud Functions的简单示例:

index.js

'use strict';

const functions = require('firebase-functions');
const { WebhookClient } = require('dialogflow-fulfillment');
const { Card, Suggestion } = require('dialogflow-fulfillment');
var axios = require("axios");

process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });
  console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));

  function welcome(agent) {
    agent.add(`Welcome to my agent!`);
  }

  function fallback(agent) {
    agent.add(`I didn't understand`);
    agent.add(`I'm sorry, can you try again?`);
  }

  function helloWorld() {
    return axios({
      method: "GET",
      url: "https://run.mocky.io/v3/197de163-acc3-4c86-a13f-79314fe9da04",
      data: "",
    })
      .then((response) => {
        console.log(response.data.body.greeting); //Hello World
        agent.add(response.data.body.greeting); 
      })
      .catch((error) => {
        console.log(error);
      });
  }

  let intentMap = new Map();
  intentMap.set('Default Welcome Intent', welcome);
  intentMap.set('Default Fallback Intent', fallback);
  intentMap.set('greeting.helloWorld', helloWorld);

  agent.handleRequest(intentMap);
});

请记住还将axios软件包添加到package.json

{
  "name": "dialogflowFirebaseFulfillment",
  "description": "This is the default fulfillment for a Dialogflow agents using Cloud Functions for Firebase",
  "version": "0.0.1",
  "private": true,
  "license": "Apache Version 2.0",
  "author": "Google Inc.",
  "engines": {
    "node": "10"
  },
  "scripts": {
    "start": "firebase serve --only functions:dialogflowFirebaseFulfillment",
    "deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"
  },
  "dependencies": {
    "actions-on-google": "^2.2.0",
    "firebase-admin": "^5.13.1",
    "firebase-functions": "^2.0.2",
    "dialogflow": "^0.6.0",
    "dialogflow-fulfillment": "^0.5.0",
    "axios": "^0.20.0"
  }
}

最后,这是我进行的帖子http查询,也许您觉得它有用。

function consulta() {
  // console.log(agent.parameters.consulta);
  // console.log(agent.query);

  var consulta = agent.query.replace(/(\r\n|\n|\r)/gm, " ");

  return axios({
    method: "POST",
    url: "http://jena-fuseki-api:3030/Matricula",
    headers: {
      Accept: "application/sparql-results+json,*/*;q=0.9",
      "Content-Type": "application/x-www-form-urlencoded",
    },
    params: {
      query: consulta,
    },
  })
    .then((response) => {
      var elements = response.data.results.bindings;

      for (var i = 0; i < elements.length; i++) {
        var result = "";
        var obj = elements[i];
        var j = 0;
        var size = Object.size(obj);
        for (var key in obj) {
          var attrName = key;
          var attrValue = obj[key].value;
          result += attrName + ": " + attrValue;
          if (j < size - 1) result += " | ";
          j++;
        }
        console.log(result);
        agent.add(result);
      }
      console.log("----------------------------");
    })
    .catch((error) => {
      console.log("Failed calling jena-fuseki API");
      console.log(error);
    });
}

Dialogflow的一些图像:

greeting intet

cloud function test

答案 1 :(得分:0)

可能还有其他问题(我没有仔细阅读您的代码),但是一个问题是,尽管您正在执行异步操作,并且在对getMyInfo()的调用中返回了Promise,但您还需要让Intent Handler searchMyInfo()返回Promise。这样,处理程序调度程序就知道在返回响应之前要等待诺言完成。

看起来也有点...奇怪...您处理响应的方式。一旦使用了dialogflow-fillfillment库,就应该使用它来生成JSON(使用agent.add()),而不是尝试自己发送JSON。我没有对此进行测试,但是可能是尝试自己发送JSON,然后让库尝试设置JSON,这可能导致Dialogflow拒绝的JSON无效。

答案 2 :(得分:0)

经过大量的试验和错误,我想出了这个index.js,它可以针对我的特定用例进行测试。如果有人想使用其他API进行尝试,我会在此处包括它。如果您进行测试,请在此处评论!我想知道它在其他情况下如何工作。

/**

* 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.
 */

'use strict';

// Include nodejs request-promise-native package as dependency
// because async API calls require the use of Promises
const rpn = require('request-promise-native'); 
const hostAPI = 'https://my.api.here/'; // root URL of the API
const {WebhookClient} = require('dialogflow-fulfillment');

exports.googleCloudSearch = (req, res) => {
  	const agent = new WebhookClient({request: req, response: res}); // Dialogflow agent
	console.log('Dialogflow Request headers: ' + JSON.stringify(req.headers)); // testing
  	console.log('Dialogflow Request body: ' + JSON.stringify(req.body)); // testing
  
  	// Default welcome intent
  	function welcome(agent) {
      agent.add('Welcome to my chatbot!');
    }

  	// Default fallback intent
  	function fallback(agent) {
      agent.add('Sorry, I don\'t understand.');
    }
    
    // Default conversation end
    function endConversation(agent) {
        agent.add('Thank you and have a nice day!');
      }
	
  	// Function for passing data to the myapi.search intent in Dialogflow
  	function searchMyApi(agent) {
		return new Promise((resolve, reject) => {
			// get parameters given by user in Dialogflow
			const param1 = agent.parameters.param1; 
            const param2 = agent.parameters.param2;	
            // and so on...

			console.log(`Parameters from Dialogflow: ${param1}, ${param2}`); // testing

            // If necessary, format the parameters passed by Dialogflow to fit the API query string.
            // Then construct the URL used to query the API.
            
            var myUrl = `${hostAPI}?parameter_1=${param1}&parameter_2=${param2}`;
			console.log('The URL is ' + myUrl); // testing

			// Make the HTTP request with request-promise-native
			// https://www.npmjs.com/package/request-promise

			var options = {
				uri: myUrl,
				headers: {
					'User-Agent': 'Request-Promise-Native'
				},
				json: true
			};
			
			// All handling of returned JSON data goes under .then and before .catch
			rpn(options)
				.then((json) => {
                    
                    var result = ''; // the answer passed to Dialogflow goes here 

                    // Make a string out of the returned JSON object
					var myStringData = JSON.stringify(json);
					console.log(`This data was returned: ${myStringData}`); // testing

					// Make an array out of the stringified JSON
                    var myArray = JSON.parse(myStringData);
                    console.log(`This is my array: ${myArray}`); // testing
                    
                    // Code for parsing myArray goes here, for example:

                    if (condition) {
                        // For example, the returned JSON does not contain the data the user wants
                        result = agent.add('Sorry, could not find any results.');
                        resolve(result); // Promise resolved
                    }
                    else {
                        // If the desired data is found:
                        var output = ''; // put the data here
                        result = agent.add(`Here are the results of your search: ${output}`);
                        resolve(result); // Promise resolved
                    }
                }) // .then end
				.catch(() => { // if .then fails
                    console.log('Promise rejected');
                    let rejectMessage= agent.add('Sorry, an error occurred.');
                    reject(rejectMessage); // Promise rejected
				});	// .catch end
        }); // Promise end
    } // searchMyApi end

  	// Mapping functions to Dialogflow intents
  	let intentMap = new Map();
  	intentMap.set('Default Welcome Intent', welcome);
    intentMap.set('Default Fallback Intent', fallback);
    intentMap.set('End Conversation', endConversation);  
  	intentMap.set('myapi.search', searchMyApi);
  	agent.handleRequest(intentMap);

}; // exports end