From ea39f807074d0b7cb195a85c16f40f313b16eaac Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Mon, 10 Sep 2018 16:24:32 -0300 Subject: [PATCH] KB is almost working in V4. --- .../services/GBConversationalService.ts | 111 ++++++------ deploy/core.gbapp/services/GBMinService.ts | 8 +- .../dialogs/FeedbackDialog.ts | 29 ++- deploy/kb.gbapp/dialogs/AskDialog.ts | 91 ++++------ deploy/kb.gbapp/dialogs/FaqDialog.ts | 2 +- deploy/kb.gbapp/dialogs/MenuDialog.ts | 2 +- deploy/kb.gbapp/services/KBService.ts | 165 +++++++++--------- 7 files changed, 191 insertions(+), 217 deletions(-) diff --git a/deploy/core.gbapp/services/GBConversationalService.ts b/deploy/core.gbapp/services/GBConversationalService.ts index 34ed0133..abb5203e 100644 --- a/deploy/core.gbapp/services/GBConversationalService.ts +++ b/deploy/core.gbapp/services/GBConversationalService.ts @@ -1,35 +1,36 @@ /*****************************************************************************\ -| ( )_ _ | -| _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | +| ( )_ _ | +| _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | | ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/' _ `\ /'_`\ | | | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| ( ) |( (_) ) | | | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' | -| | | ( )_) | | -| (_) \___/' | -| | -| General Bots Copyright (c) Pragmatismo.io. All rights reserved. | -| Licensed under the AGPL-3.0. | -| | -| According to our dual licensing model, this program can be used either | -| under the terms of the GNU Affero General Public License, version 3, | -| or under a proprietary license. | -| | -| The texts of the GNU Affero General Public License with an additional | -| permission and of our proprietary license can be found at and | -| in the LICENSE file you have received along with this program. | -| | -| This program is distributed in the hope that it will be useful, | -| but WITHOUT ANY WARRANTY; without even the implied warranty of | -| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | -| GNU Affero General Public License for more details. | -| | -| "General Bots" is a registered trademark of Pragmatismo.io. | -| The licensing of the program under the AGPLv3 does not imply a | -| trademark license. Therefore any rights, title and interest in | -| our trademarks remain entirely with us. | -| | +| | | ( )_) | | +| (_) \___/' | +| | +| General Bots Copyright (c) Pragmatismo.io. All rights reserved. | +| Licensed under the AGPL-3.0. | +| | +| According to our dual licensing model, this program can be used either | +| under the terms of the GNU Affero General Public License, version 3, | +| or under a proprietary license. | +| | +| The texts of the GNU Affero General Public License with an additional | +| permission and of our proprietary license can be found at and | +| in the LICENSE file you have received along with this program. | +| | +| This program is distributed in the hope that it will be useful, | +| but WITHOUT ANY WARRANTY; without even the implied warranty of | +| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +| GNU Affero General Public License for more details. | +| | +| "General Bots" is a registered trademark of Pragmatismo.io. | +| The licensing of the program under the AGPLv3 does not imply a | +| trademark license. Therefore any rights, title and interest in | +| our trademarks remain entirely with us. | +| | \*****************************************************************************/ + "use strict"; const logger = require("../../../src/logger"); @@ -51,7 +52,7 @@ export class GBConversationalService implements IGBConversationalService { this.coreService = coreService; } - async sendEvent(dc: any, name: string, value: any) { + async sendEvent(dc: any, name: string, value: any): Promise { const msg = MessageFactory.text(''); msg.value = value; msg.type = "event"; @@ -71,40 +72,36 @@ export class GBConversationalService implements IGBConversationalService { serviceEndpoint: min.instance.nlpServerUrl }); - return await model.recognize(dc.context).then(res => { + let res = await model.recognize(dc.context); - // Resolve intents returned from LUIS - let topIntent = LuisRecognizer.topIntent(res); + // Resolve intents returned from LUIS + let topIntent = LuisRecognizer.topIntent(res); - if (topIntent) { - var intent = topIntent; - var entity = - res.entities && res.entities.length > 0 - ? res.entities[0].entity.toUpperCase() - : null; - logger.info( - "luis: intent: [" + intent + "] entity: [" + entity + "]" - ); + if (topIntent) { + var intent = topIntent; + var entity = + res.entities && res.entities.length > 0 + ? res.entities[0].entity.toUpperCase() + : null; + logger.info( + "luis: intent: [" + intent + "] entity: [" + entity + "]" + ); - try { - dc.replace("/" + intent); - } catch (error) { - logger.info("error: intent: [" + intent + "] error: [" + error + "]"); - - dc.context.sendActivity("Desculpe-me, não encontrei nada a respeito..."); - dc.replace("/ask", { isReturning: true }); - } - - return Promise.resolve({ intent, entities: res.entities }); - } else { - - dc.context.sendActivity("Lamento, não achei nada a respeito..."); - dc.replace("/ask", { isReturning: true }); - resolve(null); + try { + await dc.replace("/" + intent); + } catch (error) { + logger.info("error: intent: [" + intent + "] error: [" + error + "]"); + await dc.context.sendActivity("Desculpe-me, não encontrei nada a respeito..."); + await dc.replace("/ask", { isReturning: true }); } + + return Promise.resolve({ intent, entities: res.entities }); + + } else { + await dc.context.sendActivity("Lamento, não achei nada a respeito..."); + await dc.replace("/ask", { isReturning: true }); + + return Promise.resolve(null); } - ).catch(err => { - return Promise.reject(new GBError(err, GBERROR_TYPE.nlpGeneralError)); - }); } } diff --git a/deploy/core.gbapp/services/GBMinService.ts b/deploy/core.gbapp/services/GBMinService.ts index e9af5320..f58af680 100644 --- a/deploy/core.gbapp/services/GBMinService.ts +++ b/deploy/core.gbapp/services/GBMinService.ts @@ -269,15 +269,19 @@ export class GBMinService { } } else if (context.activity.type === 'message') { + // Check to see if anyone replied. If not then start echo dialog + if (context.activity.text === "admin") { await dc.begin("/admin"); } else { await dc.continue(); } + } else if (context.activity.type === 'event') { + if (context.activity.name === "whoAmI") { await dc.begin("/whoAmI"); } @@ -293,8 +297,8 @@ export class GBMinService { await dc.begin("/faq"); } else if (context.activity.name === "ask") { - dc.begin("/answer", { - // TODO: query: context.activity.data, + await dc.begin("/answer", { + query: (context.activity as any).data, fromFaq: true }); } diff --git a/deploy/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts b/deploy/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts index 721c2fd6..8eda2dc9 100644 --- a/deploy/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts +++ b/deploy/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts @@ -52,8 +52,7 @@ export class FeedbackDialog extends IGBDialog { const service = new CSService(); min.dialogs.add("/feedbackNumber", [ - async (dc, args) => { - + async (dc) => { let messages = [ "O que achou do meu atendimento, de 1 a 5?", "Qual a nota do meu atendimento?", @@ -88,21 +87,15 @@ export class FeedbackDialog extends IGBDialog { await dc.prompt('textPrompt', messages[0]); }, async (dc, value) => { - AzureText.getSentiment( - min.instance.textAnalyticsKey, - value, - (err, rate) => { - if (!err && rate > 0) { - dc.context.sendActivity("Bom saber que você gostou. Conte comigo."); - } else { - dc.context.sendActivity( - "Vamos registrar sua questão, obrigado pela sinceridade." - ); - } - dc.replace('/ask', { isReturning: true }); - } - ); - } - ]); + let rate = await AzureText.getSentiment(min.instance.textAnalyticsKey, "pt-br", value); + if (rate > 0) { + dc.context.sendActivity("Bom saber que você gostou. Conte comigo."); + } else { + dc.context.sendActivity( + "Vamos registrar sua questão, obrigado pela sinceridade." + ); + } + dc.replace('/ask', { isReturning: true }); + }]); } } diff --git a/deploy/kb.gbapp/dialogs/AskDialog.ts b/deploy/kb.gbapp/dialogs/AskDialog.ts index f7b2413a..ee40f8f8 100644 --- a/deploy/kb.gbapp/dialogs/AskDialog.ts +++ b/deploy/kb.gbapp/dialogs/AskDialog.ts @@ -50,7 +50,7 @@ export class AskDialog extends IGBDialog { */ static setup(bot: BotAdapter, min: GBMinInstance) { - const service = new KBService(); + const service = new KBService(min.core.sequelize); const model = new LuisRecognizer({ appId: min.instance.nlpAppId, @@ -65,11 +65,18 @@ export class AskDialog extends IGBDialog { // Initialize values. const user = min.userState.get(dc.context); - let text = ""; + let text = args.query; + if (!text) { + throw new Error(`/answer being called with no args.query text.`) + } + + // Stops any content on projector. + + await min.conversationalService.sendEvent(dc, "stop", null); // Handle extra text from FAQ. - if (args && args.query) { + if (args && args.query) { text = args.query; } else if (args && args.fromFaq) { let messages = [ @@ -78,35 +85,31 @@ export class AskDialog extends IGBDialog { `Aguarde, por favor, enquanto acho sua resposta...` ]; - dc.context.sendActivity(messages[0]); // TODO: Handle rnd. + await dc.context.sendActivity(messages[0]); // TODO: Handle rnd. } // Spells check the input text before sending Search or NLP. - // DISABLED: - // AzureText.getSpelledText( - // min.instance.spellcheckerKey, - // text, - // async (data, err) => { - // var data = res.text; - // if (data != text) { - // logger.info("Spelled Text: " + data); - // text = data; - // } + if (min.instance.spellcheckerKey) { + let data = await AzureText.getSpelledText( + min.instance.spellcheckerKey, + text); - user.lastQuestion = text; + if (data != text) { + logger.info(`Spelling corrected: ${data}`); + text = data; + } + } // Searches KB for the first time. + user.lastQuestion = text; let resultsA = await service.ask( min.instance, text, min.instance.searchScore, user.subjects); - // Stops any content on projector. - - min.conversationalService.sendEvent(dc, "stop", null); // If there is some result, answer immediately. @@ -119,10 +122,7 @@ export class AskDialog extends IGBDialog { // Sends the answer to all outputs, including projector. - service.sendAnswer(min.conversationalService, - dc, - resultsA.answer - ); + await service.sendAnswer(min.conversationalService, dc, resultsA.answer); // Goes to ask loop, again. @@ -132,11 +132,8 @@ export class AskDialog extends IGBDialog { // Second time running Search, now with no filter. - let resultsB = await service.ask( - min.instance, - text, - min.instance.searchScore, - null); + let resultsB = await service.ask(min.instance, text, + min.instance.searchScore, null); // If there is some result, answer immediately. @@ -148,7 +145,7 @@ export class AskDialog extends IGBDialog { user.isAsking = false; user.lastQuestionId = resultsB.questionId; - // Inform user that a broader search will be used. + // Informs user that a broader search will be used. if (user.subjects.length > 0) { let subjectText = @@ -161,44 +158,32 @@ export class AskDialog extends IGBDialog { `Vou te responder de modo mais abrangente... Não apenas sobre ${subjectText}` ]; - dc.context.sendActivity(messages[0]); // TODO: Handle rnd. + await dc.context.sendActivity(messages[0]); // TODO: Handle rnd. } // Sends the answer to all outputs, including projector. - service.sendAnswer(min.conversationalService, - dc, - resultsB.answer - ); - dc.replace("/ask", { isReturning: true }); - + await service.sendAnswer(min.conversationalService, dc, resultsB.answer); + await dc.replace("/ask", { isReturning: true }); } else { - await min.conversationalService.runNLP( - dc, - min, - text, - (data, error) => { - if (!data) { - let messages = [ - "Desculpe-me, não encontrei nada a respeito.", - "Lamento... Não encontrei nada sobre isso. Vamos tentar novamente?", - "Desculpe-me, não achei nada parecido. Poderia tentar escrever de outra forma?" - ]; + let data = await min.conversationalService.runNLP(dc, min, text); + if (!data) { + let messages = [ + "Desculpe-me, não encontrei nada a respeito.", + "Lamento... Não encontrei nada sobre isso. Vamos tentar novamente?", + "Desculpe-me, não achei nada parecido. Poderia tentar escrever de outra forma?" + ]; - dc.context.sendActivity(messages[0]); // TODO: Handle rnd. - dc.replace("/ask", { isReturning: true }); - } - }).catch(err => { - console.log(err); - }); + dc.context.sendActivity(messages[0]); // TODO: Handle rnd. + dc.replace("/ask", { isReturning: true }); + } } } } ]); - min.dialogs.add("/ask", [ async (dc, args) => { const user = min.userState.get(dc.context); diff --git a/deploy/kb.gbapp/dialogs/FaqDialog.ts b/deploy/kb.gbapp/dialogs/FaqDialog.ts index 27a93968..e9508da5 100644 --- a/deploy/kb.gbapp/dialogs/FaqDialog.ts +++ b/deploy/kb.gbapp/dialogs/FaqDialog.ts @@ -46,7 +46,7 @@ export class FaqDialog extends IGBDialog { */ static setup(bot: BotAdapter, min: GBMinInstance) { - const service = new KBService(); + const service = new KBService(min.core.sequelize); min.dialogs.add("/faq", [ async (dc, args) => { diff --git a/deploy/kb.gbapp/dialogs/MenuDialog.ts b/deploy/kb.gbapp/dialogs/MenuDialog.ts index f908a4e4..cdcda5bb 100644 --- a/deploy/kb.gbapp/dialogs/MenuDialog.ts +++ b/deploy/kb.gbapp/dialogs/MenuDialog.ts @@ -60,7 +60,7 @@ export class MenuDialog extends IGBDialog { */ static setup(bot: BotAdapter, min: GBMinInstance) { - var service = new KBService(); + var service = new KBService(min.core.sequelize); bot min.dialogs.add("/menu", [ diff --git a/deploy/kb.gbapp/services/KBService.ts b/deploy/kb.gbapp/services/KBService.ts index 301bddcf..b28b563c 100644 --- a/deploy/kb.gbapp/services/KBService.ts +++ b/deploy/kb.gbapp/services/KBService.ts @@ -41,6 +41,7 @@ const path = require("path") const asyncPromise = require('async-promises') const walkPromise = require('walk-promise') +import { Sequelize } from 'sequelize-typescript' import { GBConfigService } from './../../core.gbapp/services/GBConfigService' import { GuaribasQuestion, GuaribasAnswer, GuaribasSubject } from "../models" import { IGBCoreService, IGBConversationalService, IGBInstance } from "botlib" @@ -55,6 +56,12 @@ export class KBServiceSearchResults { export class KBService { + sequelize: Sequelize + + constructor(sequelize: Sequelize) { + this.sequelize = sequelize + } + async getAnswerById( instanceId: number, answerId: number @@ -78,32 +85,26 @@ export class KBService { instanceId: number, text: string ): Promise { - return new Promise( - (resolve, reject) => { - GuaribasQuestion.findOne({ - where: { - instanceId: instanceId, - content: `%${text.trim()}%` - } - }).then((question: GuaribasQuestion) => { - if (question) { - GuaribasAnswer.findAll({ - where: { - instanceId: instanceId, - answerId: question.answerId - } - }).then((answer: GuaribasAnswer[]) => { - resolve({ question: question, answer: answer[0] }) - }) - } - else { - resolve(null) - } - }).error((reason) => { - reject(reason) - }) + const Op = Sequelize.Op + + let question = await GuaribasQuestion.findOne({ + where: { + instanceId: instanceId, + content: { [Op.like]: `%${text.trim()}%` } + } + }) + + if (question) { + let answer = await GuaribasAnswer.findOne({ + where: { + instanceId: instanceId, + answerId: question.answerId + } }) + return Promise.resolve({ question: question, answer: answer }) + } + return Promise.resolve(null) } async addAnswer(obj: GuaribasAnswer): Promise { @@ -119,26 +120,24 @@ export class KBService { async ask( instance: IGBInstance, - what: string, + query: string, searchScore: number, subjects: GuaribasSubject[] ): Promise { // Builds search query. - what = what.toLowerCase() - what = what.replace("?", " ") - what = what.replace("!", " ") - what = what.replace(".", " ") - what = what.replace("/", " ") - what = what.replace("\\", " ") + query = query.toLowerCase() + query = query.replace("?", " ") + query = query.replace("!", " ") + query = query.replace(".", " ") + query = query.replace("/", " ") + query = query.replace("\\", " ") if (subjects) { - let text = KBService.getSubjectItemsSeparatedBySpaces( - subjects - ) + let text = KBService.getSubjectItemsSeparatedBySpaces(subjects) if (text) { - what = `${what} ${text}` + query = `${query} ${text}` } } // TODO: Filter by instance. what = `${what}&$filter=instanceId eq ${instanceId}` @@ -150,7 +149,7 @@ export class KBService { instance.searchIndex, instance.searchIndexer ) - let results = await service.search(what) + let results = await service.search(query) if (results && results.length > 0 && results[0]["@search.score"] >= searchScore) { let value = await this.getAnswerById( @@ -159,7 +158,7 @@ export class KBService { return Promise.resolve({ answer: value, questionId: results[0].questionId }) } } else { - let data = await this.getAnswerByText(instance.instanceId, what) + let data = await this.getAnswerByText(instance.instanceId, query) return Promise.resolve( { answer: data.answer, questionId: data.question.questionId } ) @@ -365,7 +364,7 @@ export class KBService { delimiter: "\t" } - let data = await parse(file, opts); + let data = await parse(file, opts) return asyncPromise.eachSeries(data, async line => { // Extracts values from columns in the current line. @@ -422,7 +421,7 @@ export class KBService { content: answer, format: format, packageId: packageId - }); + }) await GuaribasQuestion.create({ from: from, to: to, @@ -434,7 +433,7 @@ export class KBService { instanceId: instanceId, answerId: answer1.answerId, packageId: packageId - }); + }) return Promise.resolve(question) @@ -448,23 +447,25 @@ export class KBService { }) } - sendAnswer(conversationalService: IGBConversationalService, - dc: any, answer: GuaribasAnswer) { + async sendAnswer(conversationalService: IGBConversationalService, + dc: any, answer: GuaribasAnswer): Promise { if (answer.content.endsWith('.mp4')) { - conversationalService.sendEvent(dc, "play", { + await conversationalService.sendEvent(dc, "play", { playerType: "video", data: answer.content }) } else if (answer.content.length > 140 && - dc.message.source != "directline") { + dc.context._activity.channelId === "webchat") { let messages = [ "Vou te responder na tela para melhor visualização...", "A resposta está na tela...", "Veja a resposta na tela..." ] - dc.context.sendActivity(messages[0]) // TODO: Handle rnd. + + await dc.context.sendActivity(messages[0]) // TODO: Handle rnd. var html = answer.content + if (answer.format === ".md") { marked.setOptions({ renderer: new marked.Renderer(), @@ -479,10 +480,10 @@ export class KBService { }) html = marked(answer.content) } - conversationalService.sendEvent(dc, "play", { playerType: "markdown", data: html }) + await conversationalService.sendEvent(dc, "play", { playerType: "markdown", data: html }) } else { - dc.context.sendActivity(answer.content) - conversationalService.sendEvent(dc, "stop", null) + await dc.context.sendActivity(answer.content) + await conversationalService.sendEvent(dc, "stop", null) } } @@ -490,26 +491,25 @@ export class KBService { localPath: string, packageStorage: GuaribasPackage, instance: IGBInstance - ): Promise { + ): Promise { // Imports subjects tree into database and return it. - return this.importSubjectFile( + await this.importSubjectFile( packageStorage.packageId, UrlJoin(localPath, "subjects.json"), - instance - ).then((value: GuaribasQuestion[]) => { + instance); - // Import all .tsv files in the tabular directory. + // Import all .tsv files in the tabular directory. - return this.importKbTabularDirectory( - localPath, - instance, - packageStorage.packageId - ) - }) + return this.importKbTabularDirectory( + localPath, + instance, + packageStorage.packageId + ) } + async importKbTabularDirectory( localPath: string, instance: IGBInstance, @@ -523,9 +523,9 @@ export class KBService { return this.importKbTabularFile( UrlJoin(file.root, file.name), instance.instanceId, - packageId); + packageId) } - })); + })) } @@ -549,13 +549,13 @@ export class KBService { title: item.title, description: item.description, packageId: packageId - }); + }) if (item.children) { return Promise.resolve(doIt(item.children, value.subjectId)) } else { - return Promise.resolve(item); + return Promise.resolve(item) } }) @@ -564,31 +564,26 @@ export class KBService { return doIt(subjects.children, null) } - undeployKbFromStorage( + async undeployKbFromStorage( instance: IGBInstance, packageId: number ) { - // TODO: call reject. - return new Promise( - (resolve, reject) => { - GuaribasQuestion.destroy({ - where: { instanceId: instance.instanceId, packageId: packageId } - }).then(value => { - GuaribasAnswer.destroy({ - where: { instanceId: instance.instanceId, packageId: packageId } - }).then(value => { - GuaribasSubject.destroy({ - where: { instanceId: instance.instanceId, packageId: packageId } - }).then(value => { - GuaribasPackage.destroy({ - where: { instanceId: instance.instanceId, packageId: packageId } - }).then(value => { - resolve(null) - }) - }) - }) - }) - }) + + await GuaribasQuestion.destroy({ + where: { instanceId: instance.instanceId, packageId: packageId } + }) + await GuaribasAnswer.destroy({ + where: { instanceId: instance.instanceId, packageId: packageId } + }) + await GuaribasSubject.destroy({ + where: { instanceId: instance.instanceId, packageId: packageId } + }) + await GuaribasPackage.destroy({ + where: { instanceId: instance.instanceId, packageId: packageId } + }) + + return Promise.resolve() + } /**