J'essaie de créer un chatbot Dialogflow qui récupère les données d'une API externe via Google Cloud Functions, mais sans utiliser Firebase. Malgré une recherche approfondie, je n'ai trouvé aucun bon exemple ou modèle de ceci; il semble que tous les exemples disponibles utilisent les fonctions Firebase.
Je suis un programmeur débutant et je ne connais pas Node.js, Promises et tous ces trucs fantaisistes, mais j'ai compris que l'accès à une API externe via Dialogflow devrait être possible même sans Firebase (j'utilise une version payante de Google Cloud).
J'ai essayé de créer ma fonction Google Cloud en utilisant cet exemple d'API météo Dialogflow, qui est la chose la plus proche de ce dont j'ai besoin, même si cela utilise également Firebase: https://github.com/dialogflow/ accomplissement-weather-nodejs / blob / master / functions / index.js # L72
Le problème est que mon code échoue quelque part autour du "res.on ('end' ..." et je ne comprends pas pourquoi. Le journal de Google Cloud Stackdriver ne donne que le message plutôt peu informatif "Ignorer l'exception d'une fonction terminée", mais ne me dit pas quelle est l'exception.
Voici un version modifiée de mon code 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
Ma question est donc la suivante: comment puis-je faire fonctionner ce code pour renvoyer la réponse d'exécution à Dialogflow? Les réponses de bienvenue et de secours par défaut proviennent de Google Cloud Functions, mais pas ma réponse de webhook d'intention personnalisée (même si "Activer l'appel de webhook" est défini dans Dialogflow pour my.search).
3 Réponses :
Il peut y avoir d'autres problèmes (je n'ai pas lu votre code trop attentivement), mais l'un est que bien que vous fassiez des opérations asynchrones, et que vous renvoyez une promesse dans votre appel à getMyInfo ()
, vous devez également demander au gestionnaire d'intention searchMyInfo ()
de renvoyer une promesse. C'est ainsi que le répartiteur du gestionnaire sait attendre que la promesse soit terminée avant de renvoyer la réponse.
Cela semble aussi un peu ... étrange ... la façon dont vous gérez la réponse. Une fois que vous utilisez la bibliothèque de traitement de flux de dialogue, vous devriez probablement l'utiliser pour générer le JSON (en utilisant agent.add ()
), plutôt que d'essayer d'envoyer le JSON vous-même. Je n'ai pas testé cela, mais il se peut qu'essayer d'envoyer le JSON vous-même, puis demander à la bibliothèque d'essayer de définir le JSON, pourrait entraîner un JSON invalide que Dialogflow rejette.
Merci beaucoup! Je suis sûr que mon code semble étrange, car il est bricolé à partir de nombreuses sources différentes que je ne comprends pas encore complètement. J'ai besoin d'un cours intensif sur Node and Promises ... Mais pour être sûr, est-il possible de faire fonctionner les appels Dialogflow vers une API tierce sans Firebase? Je commençais à douter de cela, car je ne pouvais trouver aucune source ni aucun exemple en ligne.
Absolument. Vous n'avez pas du tout besoin de Firebase. La plupart des exemples sur StackOverflow et ailleurs qui utilisent Firebase devraient fonctionner de la même manière sur n'importe quelle autre plate-forme - en particulier une fois que vous avez dépassé la partie relative à l'initialisation de la bibliothèque dialogflow-réalisation. Donc, si vous regardez simplement comment fonctionne un gestionnaire d'intention spécifique, il devrait servir d'exemple pour n'importe quelle plate-forme.
Après de nombreux essais et erreurs, j'ai créé cet index.js qui fonctionne pour mon cas d'utilisation particulier dans la mesure où j'ai pu le tester. Je l'inclus ici au cas où quelqu'un d'autre voudrait l'essayer avec une API différente. Si vous le testez, veuillez commenter ici! Je serais intéressé de savoir comment cela fonctionne pour un autre cas.
/** * 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}¶meter_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
J'ai eu le même problème et comme vous l'avez dit, je pense que c'est lié aux promesses et au comportement asynchrone de JavaScript. Parce que lorsque vous appelez une fonction cloud, celle-ci s'exécute puis répond, mais cette fonction n'attend pas l'appel d'API externe.
J'ai essayé request client, mais quand j'ai vu la vue des journaux, la réponse de l'API externe après la réponse de la fonction cloud.
J'ai donc choisi d'utiliser axios (client HTTP basé sur Promise pour node.js), alors la fonction cloud fonctionne.
Ceci est un exemple simple pour Dialogflow + API externe + fonctions Google Cloud:
index.js
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); }); }
N'oubliez pas d'ajouter axios
package à 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" } }
Enfin, c'est une requête post http que j'ai faite, peut-être que vous la trouvez utile.
XXX
Quelques images de Dialogflow: