/*****************************************************************************\ | ( )_ _ | | _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | | ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/' _ `\ /'_`\ | | | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| ( ) |( (_) ) | | | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' | | | | ( )_) | | | (_) \___/' | | | | 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. | | | \*****************************************************************************/ 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'); import { Messages } from '../strings'; import { IGBConversationalService, IGBCoreService, IGBInstance } from 'botlib'; import { AzureSearch } from 'pragmatismo-io-framework'; import { Sequelize } from 'sequelize-typescript'; import { GuaribasPackage } from '../../core.gbapp/models/GBModel'; import { GBDeployer } from '../../core.gbapp/services/GBDeployer'; import { GuaribasAnswer, GuaribasQuestion, GuaribasSubject } from '../models'; import { GBConfigService } from './../../core.gbapp/services/GBConfigService'; export class KBServiceSearchResults { public answer: GuaribasAnswer; public questionId: number; } export class KBService { public sequelize: Sequelize; constructor(sequelize: Sequelize) { this.sequelize = sequelize; } public static getFormattedSubjectItems(subjects: GuaribasSubject[]) { if (!subjects) { return ''; } const out = []; subjects.forEach(subject => { out.push(subject.title); }); return out.join(', '); } public static getSubjectItemsSeparatedBySpaces(subjects: GuaribasSubject[]) { const out = []; subjects.forEach(subject => { out.push(subject.internalId); }); return out.join(' '); } public async getQuestionById( instanceId: number, questionId: number ): Promise { return GuaribasQuestion.findOne({ where: { instanceId: instanceId, questionId: questionId } }); } public async getAnswerById( instanceId: number, answerId: number ): Promise { return GuaribasAnswer.findOne({ where: { instanceId: instanceId, answerId: answerId } }); } public async getAnswerByText( instanceId: number, text: string ): Promise { const Op = Sequelize.Op; const question = await GuaribasQuestion.findOne({ where: { instanceId: instanceId, content: { [Op.like]: `%${text.trim()}%` } } }); if (question) { const answer = await GuaribasAnswer.findOne({ where: { instanceId: instanceId, answerId: question.answerId } }); return Promise.resolve({ question: question, answer: answer }); } return Promise.resolve(null); } public async addAnswer(obj: GuaribasAnswer): Promise { return new Promise( (resolve, reject) => { GuaribasAnswer.create(obj).then(item => { resolve(item); }).error((reason) => { reject(reason); }); }); } public async ask( instance: IGBInstance, query: string, searchScore: number, subjects: GuaribasSubject[] ): Promise { // Builds search query. query = query.toLowerCase(); query = query.replace('?', ' '); query = query.replace('!', ' '); query = query.replace('.', ' '); query = query.replace('/', ' '); query = query.replace('\\', ' '); if (subjects) { const text = KBService.getSubjectItemsSeparatedBySpaces(subjects); if (text) { query = `${query} ${text}`; } } // TODO: Filter by instance. what = `${what}&$filter=instanceId eq ${instanceId}` try { if (instance.searchKey && GBConfigService.get('STORAGE_DIALECT') == 'mssql') { const service = new AzureSearch( instance.searchKey, instance.searchHost, instance.searchIndex, instance.searchIndexer ); const results = await service.search(query); if (results && results.length > 0 && results[0]['@search.score'] >= searchScore) { const value = await this.getAnswerById( instance.instanceId, results[0].answerId); if (value) { return Promise.resolve({ answer: value, questionId: results[0].questionId }); } else { return Promise.resolve({ answer: null, questionId: 0 }); } } } else { const 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 }); } } } catch (reason) { return Promise.reject(new Error(reason)); } } public async getSubjectItems( instanceId: number, parentId: number ): Promise { const where = { parentSubjectId: parentId, instanceId: instanceId }; return GuaribasSubject.findAll({ where: where }); } public async getFaqBySubjectArray(from: string, subjects: any): Promise { const where = { from: from, subject1: null, subject2: null, subject3: null, subject4:null }; if (subjects) { if (subjects[0]) { where.subject1 = subjects[0].internalId; } if (subjects[1]) { where.subject2 = subjects[1].internalId; } if (subjects[2]) { where.subject3 = subjects[2].internalId; } if (subjects[3]) { where.subject4 = subjects[3].internalId; } } return await GuaribasQuestion.findAll({ where: where }); } public async importKbTabularFile( filePath: string, instanceId: number, packageId: number ): Promise { const file = Fs.readFileSync(filePath, 'UCS-2'); const opts = { delimiter: '\t' }; let lastQuestion: GuaribasQuestion; let lastAnswer: GuaribasAnswer; const data = await parse(file, opts); return asyncPromise.eachSeries(data, async line => { // Extracts values from columns in the current line. const subjectsText = line[0]; const from = line[1]; const to = line[2]; const question = line[3]; let 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) { const 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 = ''; } } // Processes subjects hierarchy splitting by dots. const subjectArray = subjectsText.split('.'); let subject1: string, subject2: string, subject3: string, subject4: string; let 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++; }); // Now with all the data ready, creates entities in the store. const answer1 = await GuaribasAnswer.create({ instanceId: instanceId, content: answer, format: format, packageId: packageId, prevId: lastQuestion ? lastQuestion.questionId : 0 }); const question1 = await GuaribasQuestion.create({ from: from, to: to, subject1: subject1, subject2: subject2, subject3: subject3, subject4: subject4, content: question, instanceId: instanceId, answerId: answer1.answerId, packageId: packageId }); if (lastAnswer && lastQuestion) { await lastAnswer.updateAttributes({ nextId: lastQuestion.questionId }); } lastAnswer = answer1; lastQuestion = question1; return Promise.resolve(lastQuestion); } else { // Skips the header. return Promise.resolve(null); } }); } public async sendAnswer(conversationalService: IGBConversationalService, step: any, answer: GuaribasAnswer) { if (answer.content.endsWith('.mp4')) { await conversationalService.sendEvent(step, 'play', { playerType: 'video', data: answer.content }); } else if (answer.content.length > 140 && step.context._activity.channelId === 'webchat') { const locale = step.context.activity.locale; await step.context.sendActivity(Messages[locale].will_answer_projector); // TODO: Handle rnd. let html = answer.content; 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 }); html = marked(answer.content); } await conversationalService.sendEvent(step, 'play', { playerType: 'markdown', data: { content: html, answer: answer, prevId: answer.prevId, nextId: answer.nextId } }); } else { await step.context.sendActivity(answer.content); await conversationalService.sendEvent(step, 'stop', null); } } public async importKbPackage( localPath: string, packageStorage: GuaribasPackage, instance: IGBInstance ): Promise { // Imports subjects tree into database and return it. await this.importSubjectFile( packageStorage.packageId, UrlJoin(localPath, 'subjects.json'), instance); // Import all .tsv files in the tabular directory. return this.importKbTabularDirectory( localPath, instance, packageStorage.packageId ); } public async importKbTabularDirectory( localPath: string, instance: IGBInstance, packageId: number ): Promise { const 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, packageId); } })); } public async importSubjectFile( packageId: number, filename: string, instance: IGBInstance ): Promise { const subjects = JSON.parse(Fs.readFileSync(filename, 'utf8')); const doIt = async (subjects: GuaribasSubject[], parentSubjectId: number) => { return asyncPromise.eachSeries(subjects, async item => { const mediaFilename = item.id + '.png'; const 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 }); if (item.children) { return Promise.resolve(doIt(item.children, value.subjectId)); } else { return Promise.resolve(item); } }); }; return doIt(subjects.children, null); } public async undeployKbFromStorage( instance: IGBInstance, deployer: GBDeployer, packageId: number ) { 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 } }); await deployer.rebuildIndex(instance); } /** * Deploys a knowledge base to the storage using the .gbkb format. * * @param localPath Path to the .gbkb folder. */ public async deployKb(core: IGBCoreService, deployer: GBDeployer, localPath: string) { const packageType = Path.extname(localPath); const packageName = Path.basename(localPath); logger.info(`[GBDeployer] Opening package: ${localPath}`); const packageObject = JSON.parse( Fs.readFileSync(UrlJoin(localPath, 'package.json'), 'utf8') ); const instance = await core.loadInstance(packageObject.botId); logger.info(`[GBDeployer] Importing: ${localPath}`); const p = await deployer.deployPackageToStorage( instance.instanceId, packageName); await this.importKbPackage(localPath, p, instance); deployer.rebuildIndex(instance); logger.info(`[GBDeployer] Finished import of ${localPath}`); } }