KB is almost working in V4.

This commit is contained in:
Rodrigo Rodriguez 2018-09-10 16:24:32 -03:00
parent 3fdceda57c
commit ea39f80707
7 changed files with 191 additions and 217 deletions

View file

@ -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<any> {
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));
});
}
}

View file

@ -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
});
}

View file

@ -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 });
}]);
}
}

View file

@ -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);

View file

@ -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) => {

View file

@ -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", [

View file

@ -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<any> {
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<GuaribasAnswer> {
@ -119,26 +120,24 @@ export class KBService {
async ask(
instance: IGBInstance,
what: string,
query: string,
searchScore: number,
subjects: GuaribasSubject[]
): Promise<KBServiceSearchResults> {
// 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<any> {
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<GuaribasQuestion[]> {
): Promise<any> {
// 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()
}
/**