Unverified Commit b6aab9a2 authored by Renato Becker's avatar Renato Becker Committed by GitHub

Merge pull request #6 from RocketChat/connect-with-rasa-rest-api

Connect with Rasa Rest API and exchange messages + add endpoint
parents 2655b328 c38ea334
# ignore modules pulled in from npm
node_modules/
# rc-apps package output
dist/
# JetBrains IDEs
out/
.idea/
.idea_modules/
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.editorconfig
.vscode
# Apps.Rasa
Integration between Rocket.Chat and the RASA Chatbot platform
### Some Important Concepts of App.Rasa
This app can operate in 2 modes:
1. Synchronous mode
In synchronous mode, the app will make use of Rasa's REST API to exchange message. The app will make use of Rasa's [RESTInput](https://rasa.com/docs/rasa/user-guide/connectors/your-own-website/#restinput) channel.
If you are using this mode, make your you have enabled rest api in `credentials.yml` in your rasa bot. More info about it [here](https://rasa.com/docs/rasa/user-guide/connectors/your-own-website/#restinput)
2. Asynchronous / Callback Mode
In asynchronous mode, the app will make use of callbacks to receive messages from Rasa. The app will make use of Rasa's [CallbackInput](https://rasa.com/docs/rasa/user-guide/connectors/your-own-website/#callbackinput) channel.
Note that if you are using [`Reminders and External events`](https://rasa.com/docs/rasa/core/reminders-and-external-events/#reminders-and-external-events) in your Rasa Chatbot, then you will find this mode useful, as these features are only work in this mode.
If you are using this mode, you will have to make following configuration to Rasa
1. You need to supply a `credentials.yml` with the following content:
```
callback:
url: "http://localhost:3000/api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/callback
"
```
> Find the correct url in the App Details page, under API section.
### Installation steps:
#### Option 1
Download directly from Rocket.Chat marketplace
#### Option 2 (Manual Install)
1. Clone this repo and Change Directory: </br>
`git clone https://github.com/RocketChat/Apps.Rasa.git && cd Apps.Rasa/`
2. Install the required packages from `package.json`: </br>
`npm install`
3. Deploy Rocket.Chat app: </br>
`rc-apps deploy --url http://localhost:3000 --username user_username --password user_password`
Where:
- `http://localhost:3000` is your local server URL (if you are running in another port, change the 3000 to the appropriate port)
- `user_username` is the username of your admin user.
- `user_password` is the password of your admin user.
For more info refer [this](https://rocket.chat/docs/developer-guides/developing-apps/getting-started/) guide
### Rocket.Chat Apps Setup
1. First go ahead n create a Bot User. Login as administrator, then goto `Setting > Users`. There create a new Bot User. This new user should have these 2 roles.</br>
1. bot
2. livechat-agent
2. Then configure the app to automatically assign a livechat-visitor to this bot. To do so, goto `Setting > Livechat > Routing` or `Setting > Omnichannel > Routing`. There enable `Assign new conversations to bot agent` Setting.
3. The app needs some configurations to work, so to setup the app Go to `Setting > Apps > Rasa`. There, fill all the necessary fields in `SETTINGS` and click SAVE. Note all fields are required.
Some of the fields in `SETTING` include
1. Bot Username (required)
- This should contain the same bot username which we created above in Step 1
2. Rasa Server Url (required)
- URL for the Rasa Server goes here. Eg:- `http://localhost:5005`
3. Service Unavailable Message (optional)
- The Bot will send this message to Visitor if service is unavailable like suppose if no agents are online.
4. Close Chat Message (optional)
- This message will be sent automatically when a chat is closed
5. Handover Message (optional)
- The Bot will send this message to Visitor upon handover
6. Default Handover Department Name (required)
- Enter the target department name where you want to transfer the visitor upon handover. Note that you can override setting using [Handover](./docs/api-endpoints/perform-handover.md) action.
7. Enable Callbacks
- Enabling this setting will allow the app to use only callback messages. This feature is useful when you are using Reminder messages in your RASA bot.
### Apps.Rasa's API
The app provides API to trigger specific actions. The URL for the API can be found on the Apps Page(`Setting > Apps > Rasa`). Currently the app provides 2 APIs.
1. Incoming API/Endpoint
This endpoint can be used to trigger specific actions. The list of supported actions include
1. **Close Chat**<br/>
To close a chat
- REST API Documentation for this endpoint can be found [here](./docs/api-endpoints/close-chat.md)
2. **Handover**<br/>
To perform a handover
- REST API Documentation for this endpoint can be found [here](./docs/api-endpoints/perform-handover.md)
2. Callback API/Endpoint
This Endpoint is needed when the App runs in `Asynchronous / Callback Mode` mode. You will have to copy this url to `credentials.yml` file.
### Adding Quick Replies support to your Rasa Bot
- Rasa App provides out of the box support for quick replies. To add quick-replies, you can follow the structure defined in Rasa [here](https://rasa.com/docs/rasa/core/domains/#images-and-buttons)
import {
IAppAccessors,
IConfigurationExtend,
IHttp,
ILogger,
IModify,
IPersistence,
IRead,
} from '@rocket.chat/apps-engine/definition/accessors';
import { ApiSecurity, ApiVisibility } from '@rocket.chat/apps-engine/definition/api';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { ILivechatMessage } from '@rocket.chat/apps-engine/definition/livechat';
import { IPostMessageSent } from '@rocket.chat/apps-engine/definition/messages';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { settings } from './config/Settings';
import { CallbackInputEndpoint } from './endpoints/CallbackInputEndpoint';
import { IncomingEndpoint } from './endpoints/IncomingEndpoint';
import { PostMessageSentHandler } from './handler/PostMessageSentHandler';
export class RasaApp extends App implements IPostMessageSent {
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors);
}
public async executePostMessageSent(message: ILivechatMessage,
read: IRead,
http: IHttp,
persis: IPersistence,
modify: IModify): Promise<void> {
const handler = new PostMessageSentHandler(this, message, read, http, persis, modify);
await handler.run();
}
protected async extendConfiguration(configuration: IConfigurationExtend): Promise<void> {
configuration.api.provideApi({
visibility: ApiVisibility.PUBLIC,
security: ApiSecurity.UNSECURE,
endpoints: [
new IncomingEndpoint(this),
new CallbackInputEndpoint(this),
],
});
await Promise.all(settings.map((setting) => configuration.settings.provideSetting(setting)));
}
}
{
"id": "646b8e7d-f1e1-419e-9478-10d0f5bc74d9",
"version": "1.0.0",
"requiredApiVersion": "^1.15.0",
"iconFile": "icon.png",
"author": {
"name": "Rocket.Chat",
"homepage": "https://rocket.chat/",
"support": "support@rocket.chat"
},
"name": "Rasa",
"nameSlug": "rasa",
"classFile": "RasaApp.ts",
"description": "Integration between Rocket.Chat and the RASA Chatbot platform."
}
import { ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settings';
export enum AppSetting {
RasaBotUsername = 'rasa_bot_username',
RasaServerUrl = 'rasa_server_url',
RasaServiceUnavailableMessage = 'rasa_service_unavailable_message',
RasaHandoverMessage = 'rasa_handover_message',
RasaCloseChatMessage = 'rasa_close_chat_message',
RasaEnableCallbacks = 'rasa_enable_callbacks',
RasaDefaultHandoverDepartment = 'rasa_target_handover_department',
}
export enum DefaultMessage {
DEFAULT_RasaServiceUnavailableMessage = 'Sorry, I\'m having trouble answering your question.',
DEFAULT_RasaHandoverMessage = 'Transferring to an online agent',
DEFAULT_RasaCloseChatMessage = 'Closing the chat, Goodbye',
}
export const settings: Array<ISetting> = [
{
id: AppSetting.RasaBotUsername,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'bot_username',
required: true,
},
{
id: AppSetting.RasaServerUrl,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'rasa_server_url',
required: true,
},
{
id: AppSetting.RasaServiceUnavailableMessage,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'rasa_service_unavailable_message',
i18nDescription: 'rasa_service_unavailable_message_description',
required: false,
},
{
id: AppSetting.RasaCloseChatMessage,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'rasa_close_chat_message',
i18nDescription: 'rasa_close_chat_message_description',
required: false,
},
{
id: AppSetting.RasaHandoverMessage,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'rasa_handover_message',
i18nDescription: 'rasa_handover_message_description',
required: false,
},
{
id: AppSetting.RasaDefaultHandoverDepartment,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'rasa_default_handover_department',
i18nDescription: 'rasa_default_handover_department_description',
required: true,
},
{
id: AppSetting.RasaEnableCallbacks,
public: true,
type: SettingType.BOOLEAN,
packageValue: false,
value: false,
i18nLabel: 'rasa_callback_message',
i18nDescription: 'rasa_callback_message_description',
required: true,
},
];
**Close Chat**
----
Action to Close a Chat Session
* **URL**
REST API URL can be found on Apps Page <br />
Sample Url for eg: <br /> `http://localhost:3000/api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/incoming`
* **Method:**
`POST`
* **Input Data Format**
`JSON`
* **Data Params**
**Required:**
1. `action`=`close-chat` <br/>
2. `sessionId=[string]`
> Note. Session Id is the same session of Rasa
* **Success Response:**
* **Code:** 200 <br />
**Content:** `Close chat request handled successfully`
* **Error Response:**
* **Code:** 400 BAD REQUEST <br />
**Content:** <br/>
`{
error: "Error: Session Id not present in request"
}`
OR
* **Code:** 500 Internal Server Error <br />
**Content:** <br />
`{ error : "Error!! Invalid Action type" }`
OR
* **Code:** 500 Internal Server Error <br />
**Content:** <br />
`{ error : "Error occurred while processing close-chat. Details:- [Error Details]" }`
* **Sample Call:**
**Curl**
```bash
curl "http://localhost:3000/api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/incoming" \
-X POST \
-d "{\n \"sessionId\": \"2Sfq8wXw4fYPMf6r4\"\n}" \
-H "Content-Type: application/json"
```
**HTTP**
```HTTP
POST /api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/incoming HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{
"action": "close-chat",
"sessionId": "2Sfq8wXw4fYPMf6r4"
}
```
**Perform Handover**
----
Action to Perform an Handover
* **URL**
REST API URL can be found on Apps Page <br />
Sample Url for eg: <br /> `http://localhost:3000/api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/incoming`
* **Method:**
`POST`
* **Input Data Format**
`JSON`
* **Data Params**
**Required:**
1. `action` = `handover` <br/>
2. `sessionId=[string]`
> Note. Session Id is the same session of Rasa
**Optional:**
```
actionData: {
`targetDepartment=[string]`
}
```
* **Success Response:**
* **Code:** 200 <br />
**Content:** `{ result: "Perform Handover request handled successfully" }`
* **Error Response:**
* **Code:** 400 BAD REQUEST <br />
**Content:** <br/>
`{
error: "Error: Session Id not present in request"
}`
OR
* **Code:** 500 Internal Server Error <br />
**Content:** <br />
`{ error : "Error occurred while processing perform-handover. Details:- [Error Details]" }`
* **Sample Call:**
**Curl**
```bash
curl "http://localhost:3000/api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/incoming" \
-X POST \
-d "{\n \"action\": \"close-chat\",\n \"sessionId\": \"GeTEX3iLYpByZWSze\",\n \"actionData\": {\n \"targetDepartment\": \"SalesDepartment\"\n }\n}" \
-H "Content-Type: application/json" \
-H "content-length: 65"
```
**HTTP**
```HTTP
POST /api/apps/public/646b8e7d-f1e1-419e-9478-10d0f5bc74d9/incoming HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{
"action": "handover",
"sessionId": "hmZ9EGL3LFvHSeG2q",
"actionData": {
"targetDepartment": "SalesDepartment"
}
}
```
import { HttpStatusCode, IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { ApiEndpoint, IApiEndpointInfo, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api';
import { Headers, Response } from '../enum/Http';
import { Logs } from '../enum/Logs';
import { IRasaMessage } from '../enum/Rasa';
import { createHttpResponse } from '../lib/Http';
import { createRasaMessage } from '../lib/Message';
import { parseSingleRasaMessage } from '../lib/Rasa';
export class CallbackInputEndpoint extends ApiEndpoint {
public path = 'callback';
public async post(request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence): Promise<IApiResponse> {
this.app.getLogger().info(Logs.ENDPOINT_RECEIVED_REQUEST);
try {
await this.processRequest(read, modify, persis, request.content);
return createHttpResponse(HttpStatusCode.OK, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { result: Response.SUCCESS });
} catch (error) {
this.app.getLogger().error(`${ Logs.ENDPOINT_REQUEST_PROCESSING_ERROR } ${error}`);
return createHttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { error: error.message });
}
}
private async processRequest(read: IRead, modify: IModify, persis: IPersistence, endpointContent: any) {
const message: IRasaMessage = parseSingleRasaMessage(endpointContent);
await createRasaMessage(message.sessionId, read, modify, message);
}
}
import { HttpStatusCode, IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { ApiEndpoint, IApiEndpointInfo, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api';
import { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat';
import { EndpointActionNames, IActionsEndpointContent } from '../enum/Endpoints';
import { Headers, Response } from '../enum/Http';
import { Logs } from '../enum/Logs';
import { createHttpResponse } from '../lib/Http';
import { closeChat, performHandover } from '../lib/Room';
export class IncomingEndpoint extends ApiEndpoint {
public path = 'incoming';
public async post(request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence): Promise<IApiResponse> {
this.app.getLogger().info(Logs.ENDPOINT_RECEIVED_REQUEST);
try {
await this.processRequest(read, modify, persis, request.content);
return createHttpResponse(HttpStatusCode.OK, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { result: Response.SUCCESS });
} catch (error) {
this.app.getLogger().error(`${ Logs.ENDPOINT_REQUEST_PROCESSING_ERROR } ${ error }`);
return createHttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { error: error.message });
}
}
private async processRequest(read: IRead, modify: IModify, persis: IPersistence, endpointContent: IActionsEndpointContent) {
const { action, sessionId } = endpointContent;
if (!sessionId) {
throw new Error(Logs.INVALID_SESSION_ID);
}
switch (action) {
case EndpointActionNames.CLOSE_CHAT:
await closeChat(modify, read, sessionId);
break;
case EndpointActionNames.HANDOVER:
const { actionData: { targetDepartment = null } = {} } = endpointContent;
const room = await read.getRoomReader().getById(sessionId) as ILivechatRoom;
if (!room) { throw new Error(Logs.INVALID_SESSION_ID); }
const { visitor: { token: visitorToken }, department: { name = null } = {} } = room;
if (targetDepartment && name && targetDepartment === name) {
throw new Error(Logs.INVALID_ACTION_USER_ALREADY_IN_DEPARTMENT);
}
await performHandover(modify, read, sessionId, visitorToken, targetDepartment);
break;
default:
throw new Error(Logs.INVALID_ENDPOINT_ACTION);
}
}
}
export interface IActionsEndpointContent {
action: EndpointActionNames;
sessionId: string;
actionData?: {
targetDepartment?: string;
};
}
export enum EndpointActionNames {
CLOSE_CHAT = 'close-chat',
HANDOVER = 'handover',
}
export enum Headers {
CONTENT_TYPE_JSON = 'application/json',
ACCEPT_JSON = 'application/json',
}
export enum Response {
SUCCESS = 'Your request was processed successfully',
}
export enum Logs {
ENDPOINT_RECEIVED_REQUEST = 'Endpoint received a request',
INVALID_SESSION_ID = 'Error! Session Id not valid',
INVALID_ROOM_ID = 'Error! Room Id not valid',
INVALID_REQUEST_CONTENT = 'Error! Request content not valid',
INVALID_BOT_USERNAME_SETTING = 'The Bot User does not exist.',
INVALID_RASA_SERVER_URL_SETTING = 'Error! Rasa server url not valid',
INVALID_VISITOR_TOKEN = 'Error: Visitor Token not valid',
INVALID_DEPARTMENT_NAME = 'Error: Department Name is not valid',
INVALID_ACTION_USER_ALREADY_IN_DEPARTMENT = 'Error! Invalid request. User is already present in the specified department',
ENDPOINT_REQUEST_PROCESSING_ERROR = 'Error occurred while processing the request. Details:- ',
INVALID_ENDPOINT_ACTION = 'Error!! Invalid Action type',
EMPTY_BOT_USERNAME_SETTING = 'The Bot Username setting is not defined.',
RASA_REST_API_COMMUNICATION_ERROR = 'Error occurred while interacting with Rasa Rest API. Details: ',
INVALID_RESPONSE_FROM_RASA = 'Error!! Invalid Response From RASA',
INVALID_RESPONSE_FROM_RASA_CONTENT_UNDEFINED = 'Error Parsing RASA\'s Response. Content is undefined',
CLOSE_CHAT_REQUEST_FAILED_ERROR = 'Error: Internal Server Error. Could not close the chat',
HANDOVER_REQUEST_FAILED_ERROR = 'Error occurred while processing handover. Details',
INVALID_DEPARTMENT_NAME_IN_BOTH_SETTING_AND_REQUEST = 'Error: Department Name cannot be empty. Please provide a department name either in App Setting or in the handover Request',
}
export interface IRasaMessage {
message: string | IRasaQuickReplies;
sessionId: string;
}
export interface IRasaQuickReplies {
text: string;
quickReplies: Array<IRasaQuickReply>;
}
export interface IRasaQuickReply {
title: string;
payload: string;
}
import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { IApp } from '@rocket.chat/apps-engine/definition/IApp';
import { ILivechatMessage, ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { AppSetting, DefaultMessage } from '../config/Settings';
import { Logs } from '../enum/Logs';
import { IRasaMessage } from '../enum/Rasa';
import { createMessage, createRasaMessage } from '../lib/Message';
import { sendMessage } from '../lib/Rasa';
import { getAppSettingValue } from '../lib/Setting';
export class PostMessageSentHandler {
constructor(private app: IApp,
private message: ILivechatMessage,
private read: IRead,
private http: IHttp,
private persis: IPersistence,
private modify: IModify) {}
public async run() {
const { text, editedAt, room, token, sender } = this.message;
const livechatRoom = room as ILivechatRoom;
const { id: rid, type, servedBy, isOpen } = livechatRoom;
const RasaBotUsername: string = await getAppSettingValue(this.read, AppSetting.RasaBotUsername);
if (!type || type !== RoomType.LIVE_CHAT) {
return;
}
if (!isOpen || !token || editedAt || !text) {
return;
}
if (!servedBy || servedBy.username !== RasaBotUsername) {
return;
}
if (sender.username === RasaBotUsername) {
return;
}
if (!text || (text && text.trim().length === 0)) {
return;
}
let response: Array<IRasaMessage> | null;
try {
response = await sendMessage(this.read, this.http, rid, text);
} catch (error) {