botserver/deploy/kb.gbapp/services/KBService.ts

618 lines
18 KiB
TypeScript
Raw Normal View History

2018-04-21 02:59:30 -03:00
/*****************************************************************************\
| ( )_ _ |
| _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ |
| ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/' _ `\ /'_`\ |
| | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| ( ) |( (_) ) |
| | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' |
| | | ( )_) | |
| (_) \___/' |
| |
| 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, |
2018-09-11 19:40:53 -03:00
| but WITHOUT ANY WARRANTY, without even the implied warranty of |
2018-04-21 02:59:30 -03:00
| 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. |
| |
\*****************************************************************************/
2018-09-09 18:11:41 -03:00
const logger = require("../../../src/logger")
const Path = require("path")
const Fs = require("fs")
const promise = require('bluebird')
const parse = promise.promisify(require('csv-parse'))
const UrlJoin = require("url-join")
const marked = require("marked")
const path = require("path")
const asyncPromise = require('async-promises')
const walkPromise = require('walk-promise')
2018-09-10 16:24:32 -03:00
import { Sequelize } from 'sequelize-typescript'
2018-09-09 18:11:41 -03:00
import { GBConfigService } from './../../core.gbapp/services/GBConfigService'
import { GuaribasQuestion, GuaribasAnswer, GuaribasSubject } from "../models"
import { IGBCoreService, IGBConversationalService, IGBInstance } from "botlib"
import { AzureSearch } from "pragmatismo-io-framework"
import { GBDeployer } from "../../core.gbapp/services/GBDeployer"
import { GuaribasPackage } from "../../core.gbapp/models/GBModel"
2018-04-21 02:59:30 -03:00
2018-08-28 19:16:29 -03:00
export class KBServiceSearchResults {
2018-09-09 18:11:41 -03:00
answer: GuaribasAnswer
questionId: number
2018-08-28 19:16:29 -03:00
}
2018-04-21 02:59:30 -03:00
export class KBService {
2018-09-10 16:24:32 -03:00
sequelize: Sequelize
constructor(sequelize: Sequelize) {
this.sequelize = sequelize
}
2018-09-09 14:39:37 -03:00
async getAnswerById(
2018-04-21 02:59:30 -03:00
instanceId: number,
2018-09-09 14:39:37 -03:00
answerId: number
): Promise<GuaribasAnswer> {
return new Promise<GuaribasAnswer>(
(resolve, reject) => {
GuaribasAnswer.findAll({
where: {
instanceId: instanceId,
answerId: answerId
}
}).then((item: GuaribasAnswer[]) => {
2018-09-09 18:11:41 -03:00
resolve(item[0])
2018-09-09 14:39:37 -03:00
}).error((reason) => {
2018-09-09 18:11:41 -03:00
reject(reason)
})
})
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
async getAnswerByText(
2018-04-21 02:59:30 -03:00
instanceId: number,
2018-09-09 14:39:37 -03:00
text: string
): Promise<any> {
2018-09-10 16:24:32 -03:00
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
}
2018-09-09 18:11:41 -03:00
})
2018-09-10 16:24:32 -03:00
return Promise.resolve({ question: question, answer: answer })
}
return Promise.resolve(null)
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
async addAnswer(obj: GuaribasAnswer): Promise<GuaribasAnswer> {
return new Promise<GuaribasAnswer>(
(resolve, reject) => {
GuaribasAnswer.create(obj).then(item => {
2018-09-09 18:11:41 -03:00
resolve(item)
2018-09-09 14:39:37 -03:00
}).error((reason) => {
2018-09-09 18:11:41 -03:00
reject(reason)
})
})
2018-04-21 02:59:30 -03:00
}
2018-08-28 19:16:29 -03:00
async ask(
2018-04-21 02:59:30 -03:00
instance: IGBInstance,
2018-09-10 16:24:32 -03:00
query: string,
2018-04-21 02:59:30 -03:00
searchScore: number,
2018-08-28 19:16:29 -03:00
subjects: GuaribasSubject[]
): Promise<KBServiceSearchResults> {
2018-09-09 14:39:37 -03:00
// Builds search query.
2018-08-28 19:16:29 -03:00
2018-09-10 16:24:32 -03:00
query = query.toLowerCase()
query = query.replace("?", " ")
query = query.replace("!", " ")
query = query.replace(".", " ")
query = query.replace("/", " ")
query = query.replace("\\", " ")
2018-08-28 19:16:29 -03:00
2018-09-09 14:39:37 -03:00
if (subjects) {
2018-09-10 16:24:32 -03:00
let text = KBService.getSubjectItemsSeparatedBySpaces(subjects)
2018-09-09 14:39:37 -03:00
if (text) {
2018-09-10 16:24:32 -03:00
query = `${query} ${text}`
}
2018-09-09 14:39:37 -03:00
}
2018-09-09 18:11:41 -03:00
// TODO: Filter by instance. what = `${what}&$filter=instanceId eq ${instanceId}`
2018-09-09 14:39:37 -03:00
try {
2018-08-28 19:16:29 -03:00
if (instance.searchKey && GBConfigService.get("DATABASE_DIALECT") == "mssql") {
let service = new AzureSearch(
instance.searchKey,
instance.searchHost,
instance.searchIndex,
instance.searchIndexer
2018-09-09 18:11:41 -03:00
)
2018-09-10 16:24:32 -03:00
let results = await service.search(query)
2018-09-09 14:39:37 -03:00
if (results && results.length > 0 &&
results[0]["@search.score"] >= searchScore) {
let value = await this.getAnswerById(
instance.instanceId,
2018-09-09 18:11:41 -03:00
results[0].answerId)
if (value) {
return Promise.resolve({ answer: value, questionId: results[0].questionId })
}
else {
return Promise.resolve({ answer: null, questionId: 0 })
}
2018-09-09 14:39:37 -03:00
}
2018-08-28 19:16:29 -03:00
} else {
2018-09-10 16:24:32 -03:00
let data = await this.getAnswerByText(instance.instanceId, query)
if (data) {
return Promise.resolve(
{ answer: data.answer, questionId: data.question.questionId })
} else {
return Promise.resolve({ answer: null, questionId: 0 })
}
2018-08-28 19:16:29 -03:00
}
2018-09-09 14:39:37 -03:00
}
catch (reason) {
2018-09-09 18:11:41 -03:00
return Promise.reject(reason)
2018-09-09 14:39:37 -03:00
}
2018-04-21 02:59:30 -03:00
}
getSearchSchema(indexName) {
return {
name: indexName,
2018-04-21 02:59:30 -03:00
fields: [
{
name: "questionId",
type: "Edm.String",
searchable: false,
filterable: false,
retrievable: true,
sortable: false,
facetable: false,
key: true
},
{
name: "subject1",
type: "Edm.String",
searchable: true,
filterable: false,
retrievable: false,
sortable: false,
facetable: false,
key: false
},
{
name: "subject2",
type: "Edm.String",
searchable: true,
filterable: false,
retrievable: false,
sortable: false,
facetable: false,
key: false
},
{
name: "subject3",
type: "Edm.String",
searchable: true,
filterable: false,
retrievable: false,
sortable: false,
facetable: false,
key: false
},
{
name: "subject4",
type: "Edm.String",
searchable: true,
filterable: false,
retrievable: false,
sortable: false,
facetable: false,
key: false
},
{
name: "content",
type: "Edm.String",
searchable: true,
filterable: false,
retrievable: false,
sortable: false,
facetable: false,
key: false
},
{
name: "answerId",
type: "Edm.Int32",
searchable: false,
filterable: false,
retrievable: true,
sortable: false,
facetable: false,
key: false
},
{
name: "instanceId",
type: "Edm.Int32",
searchable: false,
filterable: true,
retrievable: true,
sortable: false,
facetable: false,
key: false
},
{
name: "packageId",
type: "Edm.Int32",
searchable: false,
filterable: true,
retrievable: true,
sortable: false,
facetable: false,
key: false
}
],
scoringProfiles: [],
defaultScoringProfile: null,
corsOptions: null
2018-09-09 18:11:41 -03:00
}
2018-04-21 02:59:30 -03:00
}
static getFormattedSubjectItems(subjects: GuaribasSubject[]) {
2018-09-09 18:11:41 -03:00
if (!subjects) return ""
let out = []
2018-04-21 02:59:30 -03:00
subjects.forEach(subject => {
2018-09-09 18:11:41 -03:00
out.push(subject.title)
})
return out.join(", ")
2018-04-21 02:59:30 -03:00
}
static getSubjectItemsSeparatedBySpaces(subjects: GuaribasSubject[]) {
2018-09-09 18:11:41 -03:00
let out = []
2018-04-21 02:59:30 -03:00
subjects.forEach(subject => {
2018-09-09 18:11:41 -03:00
out.push(subject.title)
})
return out.join(" ")
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
async getSubjectItems(
2018-04-21 02:59:30 -03:00
instanceId: number,
2018-09-09 14:39:37 -03:00
parentId: number
): Promise<GuaribasSubject[]> {
return new Promise<GuaribasSubject[]>(
(resolve, reject) => {
2018-09-09 18:11:41 -03:00
var where = { parentSubjectId: parentId, instanceId: instanceId }
2018-09-09 14:39:37 -03:00
GuaribasSubject.findAll({
where: where
})
.then((values: GuaribasSubject[]) => {
2018-09-09 18:11:41 -03:00
resolve(values)
2018-09-09 14:39:37 -03:00
})
.error(reason => {
2018-09-09 18:11:41 -03:00
reject(reason)
})
})
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
async getFaqBySubjectArray(from: string, subjects: any): Promise<GuaribasQuestion[]> {
return new Promise<GuaribasQuestion[]>(
(resolve, reject) => {
2018-04-21 02:59:30 -03:00
2018-09-09 14:39:37 -03:00
let where = {
from: from
2018-09-09 18:11:41 -03:00
}
2018-04-21 02:59:30 -03:00
2018-09-09 14:39:37 -03:00
if (subjects) {
if (subjects[0]) {
2018-09-09 18:11:41 -03:00
where["subject1"] = subjects[0].title
2018-09-09 14:39:37 -03:00
}
2018-04-21 02:59:30 -03:00
2018-09-09 14:39:37 -03:00
if (subjects[1]) {
2018-09-09 18:11:41 -03:00
where["subject2"] = subjects[1].title
2018-09-09 14:39:37 -03:00
}
2018-04-21 02:59:30 -03:00
2018-09-09 14:39:37 -03:00
if (subjects[2]) {
2018-09-09 18:11:41 -03:00
where["subject3"] = subjects[2].title
2018-09-09 14:39:37 -03:00
}
if (subjects[3]) {
2018-09-09 18:11:41 -03:00
where["subject4"] = subjects[3].title
2018-09-09 14:39:37 -03:00
}
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
GuaribasQuestion.findAll({
where: where
})
.then((items: GuaribasQuestion[]) => {
2018-09-09 18:11:41 -03:00
if (!items) items = []
2018-09-09 14:39:37 -03:00
if (items.length == 0) {
2018-09-09 18:11:41 -03:00
resolve([])
2018-09-09 14:39:37 -03:00
} else {
2018-09-09 18:11:41 -03:00
resolve(items)
2018-09-09 14:39:37 -03:00
}
})
.catch(reason => {
if (reason.message.indexOf("no such table: IGBInstance") != -1) {
2018-09-09 18:11:41 -03:00
resolve([])
2018-09-09 14:39:37 -03:00
} else {
2018-09-09 18:11:41 -03:00
reject(reason)
logger.info(`GuaribasServiceError: ${reason}`)
2018-09-09 14:39:37 -03:00
}
2018-09-09 18:11:41 -03:00
})
})
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
async importKbTabularFile(
filePath: string,
2018-04-21 02:59:30 -03:00
instanceId: number,
2018-09-09 14:39:37 -03:00
packageId: number
): Promise<GuaribasQuestion[]> {
let file = Fs.readFileSync(filePath, "UCS-2")
let opts = {
delimiter: "\t"
}
2018-09-10 16:24:32 -03:00
let data = await parse(file, opts)
return asyncPromise.eachSeries(data, async line => {
// Extracts values from columns in the current line.
let subjectsText = line[0]
var from = line[1]
var to = line[2]
var question = line[3]
var answer = line[4]
// Skips the first line.
if (!(subjectsText === "subjects" && from == "from")) {
let format = ".txt"
// Extracts answer from external media if any.
if (answer.indexOf(".md") > -1) {
let mediaFilename = UrlJoin(path.dirname(filePath), "..", "articles", answer)
if (Fs.existsSync(mediaFilename)) {
answer = Fs.readFileSync(mediaFilename, "utf8")
format = ".md"
} else {
logger.info(`[GBImporter] File not found: ${mediaFilename}.`)
answer =
"Por favor, contate a administração para rever esta pergunta."
}
2018-09-09 18:11:41 -03:00
}
2018-09-09 14:39:37 -03:00
// Processes subjects hierarchy splitting by dots.
let subjectArray = subjectsText.split(".")
let subject1: string, subject2: string, subject3: string,
subject4: string
var indexer = 0
subjectArray.forEach(element => {
if (indexer == 0) {
subject1 = subjectArray[indexer].substring(0, 63)
} else if (indexer == 1) {
subject2 = subjectArray[indexer].substring(0, 63)
} else if (indexer == 2) {
subject3 = subjectArray[indexer].substring(0, 63)
} else if (indexer == 3) {
subject4 = subjectArray[indexer].substring(0, 63)
}
indexer++
})
2018-09-09 18:11:41 -03:00
// Now with all the data ready, creates entities in the store.
let answer1 = await GuaribasAnswer.create({
instanceId: instanceId,
content: answer,
format: format,
packageId: packageId
2018-09-10 16:24:32 -03:00
})
await GuaribasQuestion.create({
from: from,
to: to,
subject1: subject1,
subject2: subject2,
subject3: subject3,
subject4: subject4,
content: question,
instanceId: instanceId,
answerId: answer1.answerId,
packageId: packageId
2018-09-10 16:24:32 -03:00
})
2018-09-10 12:09:48 -03:00
return Promise.resolve(question)
} else {
// Skips the header.
return Promise.resolve(null)
}
})
2018-04-21 02:59:30 -03:00
}
2018-09-10 16:24:32 -03:00
async sendAnswer(conversationalService: IGBConversationalService,
dc: any, answer: GuaribasAnswer): Promise<any> {
2018-04-21 02:59:30 -03:00
if (answer.content.endsWith('.mp4')) {
2018-09-10 16:24:32 -03:00
await conversationalService.sendEvent(dc, "play", {
2018-04-21 02:59:30 -03:00
playerType: "video",
data: answer.content
2018-09-09 18:11:41 -03:00
})
2018-09-09 14:39:37 -03:00
} else if (answer.content.length > 140 &&
2018-09-10 16:24:32 -03:00
dc.context._activity.channelId === "webchat") {
let messages = [
2018-04-21 02:59:30 -03:00
"Vou te responder na tela para melhor visualização...",
"A resposta está na tela...",
"Veja a resposta na tela..."
2018-09-09 18:11:41 -03:00
]
2018-09-10 16:24:32 -03:00
await dc.context.sendActivity(messages[0]) // TODO: Handle rnd.
2018-09-09 18:11:41 -03:00
var html = answer.content
2018-09-10 16:24:32 -03:00
2018-04-21 02:59:30 -03:00
if (answer.format === ".md") {
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false
2018-09-09 18:11:41 -03:00
})
html = marked(answer.content)
2018-04-21 02:59:30 -03:00
}
2018-09-10 16:24:32 -03:00
await conversationalService.sendEvent(dc, "play", { playerType: "markdown", data: html })
2018-04-21 02:59:30 -03:00
} else {
2018-09-10 16:24:32 -03:00
await dc.context.sendActivity(answer.content)
await conversationalService.sendEvent(dc, "stop", null)
2018-04-21 02:59:30 -03:00
}
}
2018-09-09 14:39:37 -03:00
async importKbPackage(
2018-04-21 02:59:30 -03:00
localPath: string,
packageStorage: GuaribasPackage,
instance: IGBInstance
2018-09-10 16:24:32 -03:00
): Promise<any> {
2018-09-09 14:39:37 -03:00
2018-09-09 18:11:41 -03:00
// Imports subjects tree into database and return it.
2018-09-09 14:39:37 -03:00
2018-09-10 16:24:32 -03:00
await this.importSubjectFile(
2018-09-09 18:11:41 -03:00
packageStorage.packageId,
UrlJoin(localPath, "subjects.json"),
2018-09-11 19:40:53 -03:00
instance)
2018-09-09 14:39:37 -03:00
2018-09-10 16:24:32 -03:00
// Import all .tsv files in the tabular directory.
2018-09-09 14:39:37 -03:00
2018-09-10 16:24:32 -03:00
return this.importKbTabularDirectory(
localPath,
instance,
packageStorage.packageId
)
2018-04-21 02:59:30 -03:00
}
2018-09-10 16:24:32 -03:00
2018-09-09 18:11:41 -03:00
async importKbTabularDirectory(
2018-04-21 02:59:30 -03:00
localPath: string,
instance: IGBInstance,
packageId: number
2018-09-09 18:11:41 -03:00
): Promise<any> {
let files = await walkPromise(UrlJoin(localPath, "tabular"))
return Promise.all(files.map(async file => {
if (file.name.endsWith(".tsv")) {
return this.importKbTabularFile(
UrlJoin(file.root, file.name),
instance.instanceId,
2018-09-10 16:24:32 -03:00
packageId)
2018-09-09 18:11:41 -03:00
}
2018-09-10 16:24:32 -03:00
}))
2018-09-09 14:39:37 -03:00
2018-04-21 02:59:30 -03:00
}
2018-09-09 14:39:37 -03:00
async importSubjectFile(
2018-04-21 02:59:30 -03:00
packageId: number,
filename: string,
instance: IGBInstance
): Promise<any> {
var subjects = JSON.parse(Fs.readFileSync(filename, "utf8"))
const doIt = async (subjects: GuaribasSubject[], parentSubjectId: number) => {
return asyncPromise.eachSeries(subjects, async item => {
let mediaFilename = item.id + ".png"
let value = await GuaribasSubject.create({
internalId: item.id,
parentSubjectId: parentSubjectId,
instanceId: instance.instanceId,
from: item.from,
to: item.to,
title: item.title,
description: item.description,
packageId: packageId
2018-09-10 16:24:32 -03:00
})
if (item.children) {
return Promise.resolve(doIt(item.children, value.subjectId))
}
else {
2018-09-10 16:24:32 -03:00
return Promise.resolve(item)
}
2018-09-09 14:39:37 -03:00
2018-09-09 18:11:41 -03:00
})
}
return doIt(subjects.children, null)
2018-04-21 02:59:30 -03:00
}
2018-09-10 16:24:32 -03:00
async undeployKbFromStorage(
2018-04-21 02:59:30 -03:00
instance: IGBInstance,
2018-09-09 14:39:37 -03:00
packageId: number
2018-04-21 02:59:30 -03:00
) {
2018-09-10 16:24:32 -03:00
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()
2018-04-21 02:59:30 -03:00
}
/**
2018-09-09 14:39:37 -03:00
* Deploys a knowledge base to the storage using the .gbkb format.
*
* @param localPath Path to the .gbkb folder.
*/
async deployKb(core: IGBCoreService, deployer: GBDeployer, localPath: string) {
2018-09-09 18:11:41 -03:00
let packageType = Path.extname(localPath)
let packageName = Path.basename(localPath)
logger.info(`[GBDeployer] Opening package: ${localPath}`)
2018-04-21 02:59:30 -03:00
let packageObject = JSON.parse(
Fs.readFileSync(UrlJoin(localPath, "package.json"), "utf8")
2018-09-09 18:11:41 -03:00
)
2018-04-21 02:59:30 -03:00
2018-09-09 18:11:41 -03:00
let instance = await core.loadInstance(packageObject.botId)
logger.info(`[GBDeployer] Beginning importing: ${localPath}`)
2018-09-09 14:39:37 -03:00
let p = await deployer.deployPackageToStorage(
instance.instanceId,
2018-09-09 18:11:41 -03:00
packageName)
await this.importKbPackage(localPath, p, instance)
logger.info(`[GBDeployer] Finished importing ${localPath}`)
2018-04-21 02:59:30 -03:00
}
}