From e5a933094f290cac8b09611b66f960fcee3f9a42 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 24 Jun 2020 01:39:50 +0530 Subject: [PATCH] [NEW] connect with Rasa's rest api + multi-user support --- AppsRasaApp.ts | 9 +++- definition/IParsedRasaResponse.ts | 3 ++ handler/PostLivechatAgentAssignedHandler.ts | 38 +++++++++++++++ handler/PostMessageSentHandler.ts | 25 +++++++++- lib/AppSettings.ts | 10 ++++ lib/RasaSDK.ts | 53 +++++++++++++++++++++ lib/persistence.ts | 23 +++++++++ 7 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 definition/IParsedRasaResponse.ts create mode 100644 handler/PostLivechatAgentAssignedHandler.ts create mode 100644 lib/RasaSDK.ts create mode 100644 lib/persistence.ts diff --git a/AppsRasaApp.ts b/AppsRasaApp.ts index d6f7886..7ffed0b 100644 --- a/AppsRasaApp.ts +++ b/AppsRasaApp.ts @@ -9,12 +9,14 @@ import { IRead, } from '@rocket.chat/apps-engine/definition/accessors'; import { App } from '@rocket.chat/apps-engine/definition/App'; +import { ILivechatEventContext, IPostLivechatAgentAssigned } from '@rocket.chat/apps-engine/definition/livechat'; import { IMessage, IPostMessageSent } from '@rocket.chat/apps-engine/definition/messages'; import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import { PostLivechatAgentAssignedHandler } from './handler/PostLivechatAgentAssignedHandler'; import { PostMessageSentHandler } from './handler/PostMessageSentHandler'; import { AppSettings } from './lib/AppSettings'; -export class AppsRasaApp extends App implements IPostMessageSent { +export class AppsRasaApp extends App implements IPostMessageSent, IPostLivechatAgentAssigned { constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } @@ -37,4 +39,9 @@ export class AppsRasaApp extends App implements IPostMessageSent { } } + public async executePostLivechatAgentAssigned(context: ILivechatEventContext, read: IRead, http: IHttp, persistence: IPersistence): Promise { + const postLivechatAgentAssignedHandler = new PostLivechatAgentAssignedHandler(context, read, http, persistence); + await postLivechatAgentAssignedHandler.run(); + } + } diff --git a/definition/IParsedRasaResponse.ts b/definition/IParsedRasaResponse.ts new file mode 100644 index 0000000..a9186d4 --- /dev/null +++ b/definition/IParsedRasaResponse.ts @@ -0,0 +1,3 @@ +export interface IParsedRasaResponse { + messages: Array; +} diff --git a/handler/PostLivechatAgentAssignedHandler.ts b/handler/PostLivechatAgentAssignedHandler.ts new file mode 100644 index 0000000..d6adb62 --- /dev/null +++ b/handler/PostLivechatAgentAssignedHandler.ts @@ -0,0 +1,38 @@ +import { ILivechatEventContext, ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; + +import { IHttp, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { getAppSetting } from '../helper'; +import { AppSettingId } from '../lib/AppSettings'; +import { AppPersistence } from '../lib/persistence'; + +export class PostLivechatAgentAssignedHandler { + constructor(private context: ILivechatEventContext, + private read: IRead, + private http: IHttp, + private persis: IPersistence) {} + + public async run() { + const SettingBotUsername: string = await getAppSetting(this.read, AppSettingId.RasaBotUsername); + if (SettingBotUsername !== this.context.agent.username) { return; } + + await this.saveVisitorSession(); + } + + /** + * + * @description - save visitor.token and session id. + * - This will provide a mapping between visitor.token n session id. + * - This is required for implementing `perform-handover` webhooks since it requires a Visitor object + * which can be obtained from using visitor.token we save here in Persistant storage + */ + private async saveVisitorSession() { + const persistence = new AppPersistence(this.persis, this.read.getPersistenceReader()); + + const lroom = this.context.room as ILivechatRoom; + if (!lroom) { throw new Error('Error!! Could not create session. room object is undefined'); } + + // session Id for Dialogflow will be the same as Room Id + const sessionId = lroom.id; + await persistence.saveSessionId(sessionId); + } +} diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index 59c4522..e220039 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -2,8 +2,11 @@ import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/de import { IApp } from '@rocket.chat/apps-engine/definition/IApp'; import { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { getAppSetting, getBotUser } from '../helper'; +import { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { IParsedRasaResponse } from '../definition/IParsedRasaResponse'; +import { getAppSetting, getBotUser, getSessionId } from '../helper'; import { AppSettingId } from '../lib/AppSettings'; +import { RasaSDK } from '../lib/RasaSDK'; export class PostMessageSentHandler { constructor(private app: IApp, @@ -27,5 +30,25 @@ export class PostMessageSentHandler { } // send request to Rasa + if (!this.message.text || (this.message.text && this.message.text.trim().length === 0)) { return; } + const messageText: string = this.message.text; + + const sessionId = getSessionId(this.message); + const rasaSDK: RasaSDK = new RasaSDK(this.http, this.read, this.persis, sessionId, messageText); + + const response: IParsedRasaResponse = await rasaSDK.sendMessage(); + for (const message of response.messages) { + await this.sendMessageToVisitor(message); + } + + } + + private async sendMessageToVisitor(message: string) { + const sender: IUser = getBotUser(this.message); + + // build the message for Livechat widget + const builder = this.modify.getNotifier().getMessageBuilder(); + builder.setRoom(this.message.room).setText(message).setSender(sender); + await this.modify.getCreator().finish(builder); } } diff --git a/lib/AppSettings.ts b/lib/AppSettings.ts index c4d074c..ad7dd5b 100644 --- a/lib/AppSettings.ts +++ b/lib/AppSettings.ts @@ -2,6 +2,7 @@ import { ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settin export enum AppSettingId { RasaBotUsername = 'rasa_bot_username', + RasaServerUrl = 'rasa_server_url', } export const AppSettings: Array = [ @@ -13,4 +14,13 @@ export const AppSettings: Array = [ i18nLabel: 'Bot Username', required: true, }, + { + id: AppSettingId.RasaServerUrl, + public: true, + type: SettingType.STRING, + packageValue: '', + i18nLabel: 'Rasa Server Url', + i18nDescription: 'Here enter the RASA url where the RASA server is hosted. Make sure to add `/webhooks/rest/webhook` to the end of url. Eg: https://efee760b.ngrok.io/webhooks/rest/webhook', + required: true, + }, ]; diff --git a/lib/RasaSDK.ts b/lib/RasaSDK.ts new file mode 100644 index 0000000..13a8b78 --- /dev/null +++ b/lib/RasaSDK.ts @@ -0,0 +1,53 @@ +import { IHttp, IHttpRequest, IHttpResponse, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { IParsedRasaResponse } from '../definition/IParsedRasaResponse'; +import { getAppSetting } from '../helper'; +import { AppSettingId } from './AppSettings'; + +export class RasaSDK { + + constructor(private http: IHttp, + private read: IRead, + private persis: IPersistence, + private sessionId: string, + private messageText: string) {} + + public async sendMessage(): Promise { + const rasaServerUrl = await getAppSetting(this.read, AppSettingId.RasaServerUrl); + + const httpRequestContent: IHttpRequest = this.buildRasaHTTPRequest(); + + // send request to dialogflow + const response = await this.http.post(rasaServerUrl, httpRequestContent); + if (response.statusCode !== 200) { throw Error(`Error occured while interacting with Rasa Rest API. Details: ${response.content}`); } + + const parsedMessage = this.parseRasaResponse(response); + return parsedMessage; + } + + private parseRasaResponse(response: IHttpResponse): IParsedRasaResponse { + if (!response.content) { throw new Error('Error Parsing Dialogflow\'s Response. Content is undefined'); } + const responseJSON = JSON.parse(response.content); + + const messages: Array = []; + + responseJSON.forEach((element) => { + messages.push(element.text); + }); + + return { + messages, + }; + } + + private buildRasaHTTPRequest(): IHttpRequest { + return { + headers: { + 'Content-Type': 'application/json', + }, + data: { + sender: this.sessionId, + message: this.messageText, + }, + }; + } +} diff --git a/lib/persistence.ts b/lib/persistence.ts new file mode 100644 index 0000000..c8c7093 --- /dev/null +++ b/lib/persistence.ts @@ -0,0 +1,23 @@ +import { IPersistence, IPersistenceRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; + +export class AppPersistence { + constructor(private readonly persistence: IPersistence, private readonly persistenceRead: IPersistenceRead) {} + + public async saveSessionId(sessionId: string): Promise { + const sessionIdAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, sessionId); + const sessionAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, 'session-Id'); + + await this.persistence.updateByAssociations([sessionIdAssociation, sessionAssociation], { + sessionId, + }, true); + } + + public async checkIfSessionExists(sessionId: string): Promise { + const sessionIdAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, sessionId); + const sessionAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, 'session-Id'); + + const [result] = await this.persistenceRead.readByAssociations([sessionIdAssociation, sessionAssociation]); + return result && (result as any).sessionId; + } +} -- GitLab