Skip to content
Merged
5 changes: 4 additions & 1 deletion DialogflowApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { OnAgentAssignedHandler } from './handler/OnAgentAssignedHandler';
import { OnAgentUnassignedHandler } from './handler/OnAgentUnassignedHandler';
import { OnSettingUpdatedHandler } from './handler/OnSettingUpdatedHandler';
import { PostMessageSentHandler } from './handler/PostMessageSentHandler';
import { EventScheduler } from './lib/EventTimeoutProcessor';

import { SessionMaintenanceProcessor } from './lib/sessionMaintenance/SessionMaintenanceProcessor';

Expand Down Expand Up @@ -87,7 +88,9 @@ export class DialogflowApp extends App implements IPostMessageSent, IPostLivecha
new FulfillmentsEndpoint(this),
],
});
await configuration.scheduler.registerProcessors([new SessionMaintenanceProcessor('session-maintenance')]);

await configuration.scheduler.registerProcessors([new SessionMaintenanceProcessor('session-maintenance'),
new EventScheduler('event-scheduler')]);

await Promise.all(settings.map((setting) => configuration.settings.provideSetting(setting)));
}
Expand Down
35 changes: 34 additions & 1 deletion config/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ISetting, SettingType } from '@rocket.chat/apps-engine/definition/settings';

export enum AppSetting {
DialogflowBotUsername = 'dialogflow_bot_username',
DialogflowBotId = 'dialogflow_bot_id',
DialogflowProjectId = 'dialogflow_project_id',
DialogflowVersion = 'dialog_flow_version',
DialogflowClientEmail = 'dialogflow_client_email',
DialogFlowPrivateKey = 'dialogflow_private_key',
DialogflowEnvironment = 'dialogflow_environment',
Expand All @@ -25,6 +25,9 @@ export enum AppSetting {
DialogflowSessionMaintenanceInterval = 'dialogflow_session_maintenance_interval',
DialogflowSessionMaintenanceEventName = 'dialogflow_session_maintenance_event_name',
DialogflowLogLevel = 'log_level',
DialogflowAgentId = 'dialogflow_cx_agent_id',
DialogflowRegion = 'dialogflow_cx_region',
DialogflowCXFallbackEvents = 'dialogflow_cx_fallback_events',
}

export enum DefaultMessage {
Expand All @@ -38,6 +41,18 @@ export enum DefaultMessage {
}

export const settings: Array<ISetting> = [
{
id: AppSetting.DialogflowVersion,
public: true,
type: SettingType.SELECT,
packageValue: 'ES',
i18nLabel: 'agent_version',
values: [
{ key: 'ES', i18nLabel: 'ES' },
{ key: 'CX', i18nLabel: 'CX' },
],
required: true,
},
{
id: AppSetting.DialogflowBotUsername,
public: true,
Expand Down Expand Up @@ -89,6 +104,24 @@ export const settings: Array<ISetting> = [
i18nDescription: 'dialogflow_environment_description',
required: false,
},
{
id: AppSetting.DialogflowAgentId,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'dialogflow_cx_agent_id',
i18nDescription: 'dialogflow_cx_agent_id_desc',
required: false,
},
{
id: AppSetting.DialogflowRegion,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'dialogflow_cx_region',
i18nDescription: 'dialogflow_cx_region_desc',
required: false,
},
{
id: AppSetting.DialogflowFallbackResponsesLimit,
public: true,
Expand Down
50 changes: 50 additions & 0 deletions docs/CXAgent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Set Up For Dialogflow CX Agent <img src="https://gstatic.com/dialogflow-console/common/assets/ccai-icon-family/dialogflow-cx-512-color.png" width="75" />

## Section I: Setting up project

### Set up for users with no existing google cloud project

- Go to the [Dialogflow CX platform](https://dialogflow.cloud.google.com/cx/projects) and create a new project. <br>

<img src="https://i.imgur.com/7TNUxl0.png" width="75%" />

- Generate a key with the services credentials from `Service Accounts -> Actions -> Manage keys -> Add Key -> Create new key` with JSON key type <br>

<img src="https://i.imgur.com/HuwAbZp.png" width="75%"/>

- Create a Billing Account with a specificied payment method on your Google platform in `Billing -> Manage Billing Accounts -> Create Account`. Setup up a billing profile and activate billing.

- From your Google Cloud Platform dashboard enable the Dialogflow API from `APIs & Services -> ENABLE APIS AND SERVICES`, search for **Dialogflow Api**, select it and click enable.<br>

<img src="https://i.imgur.com/Sv6I1x3.png" width="75%"/>

### Set up for users with existing google cloud project

- Create a Billing Account with a specificied payment method on your Google platform in `Billing -> Manage Billing Accounts -> Create Account`. Setup up a billing profile and activate billing.
- From your Google Cloud Platform dashboard enable the Dialogflow API from `APIs & Services -> ENABLE APIS AND SERVICES`, search for **Dialogflow Api**, select it and click enable.

<hr>

## Section II: Create Agent

- From the [Dialogflow CX platform](https://dialogflow.cloud.google.com/cx/projects) select the project and create a new agent.

<hr>

## Section III: Setting up Apps.Dialogflow


- Set agent version to **CX**. <br> <img src="https://i.imgur.com/f5dhWIp.png" weight="600"/>
- From the [Dialogflow CX platform](https://dialogflow.cloud.google.com/cx/projects) select the project and from your agent list press <button>⋮</button> and select **Copy Name**.
- The agent name has the following format: <br>
<b>projects/PROJECT_ID/locations/<mark style="color: red">REGION_ID</mark>/agents/<mark style="color: red">AGENT_ID</mark></b>
- Paste **AGENT_ID** to **Dialogflow Agent ID** and **REDION ID** to **Dialogflow Region**. <br>
<img src="https://i.imgur.com/TCAkRQs.png" width="75%"/>

<hr>

## Section IV: Additional Settings

- Dialogflow CX does not support fallback Intents. You can create a list of comma-separated event names in **Fallback Events** that will be treated as fallback events by the app.


90 changes: 90 additions & 0 deletions docs/ES-CX comparison.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Dialogflow ES | CX comparison

This document files the re-work done in Apps.Dialogflow in order to fully support the functionality of a CX agent and satisfy the requirements of the [Chatbot Client Replacement](https://github.com/WideChat/Rocket.Chat/projects/3) project.

## Introduction
### 1. Dialogflow ES
* Uses intents and context to control the flow of the conversation.
* Needs to use webhooks to send information to the application for calculations.

### 2. Dialogflow CX
* Pages with routes to control the flow. A page is a container that can contain multiple routes, events.
* Intents have been simplified for reusability. They now only contain their name and relevant training phrases.
* Each route contains one intent. If a route's intent inside a page is triggered then the route will direct the flow to a different page. ![enter image description here](https://i.imgur.com/KyL46h2.png)
* Webhooks use is not as necessary. Logic is handled inside the route / event.
* Routes can be conditional.
* Session parameters are used to capture and reference values that have been supplied by the end-user during a session.
* Entity syntax is changed.
* Multiple flows can be constructed for complex agents.

## Connection & Authentication

### 1. Dialogflow ES

- Dialogflow ES requires an **access token** as the URL parameter of the API call to the server.
- Dialogflow ES is available GoogleAPIs **v2**.

***SERVER URL***
``https://dialogflow.googleapis.com/v2/projects/${projectId}/agent/environments/${environment || 'draft'}/users/-/sessions/${sessionId}:detectIntent?access_token=${accessToken}``

### 2. Dialogflow CX

- Dialogflow CX requires an **access token** in the header of the HTTP request.
***HEADER***
``{ 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON, 'Authorization': 'Bearer ' + accessToken }``
- Dialogflow CX is available GoogleAPIs **v3**.
- The CX agent server URL requires an additional **regionID** and **agentID** both are found in the agent's full name in the [Dialogflow CX console](https://dialogflow.cloud.google.com/cx/projects) with the following format:
>projects/PROJECT_ID/locations/***REGION_ID***/agents/***AGENT_ID***

***SERVER URL***
``https://${regionId}-dialogflow.googleapis.com/v3/projects/${projectId}/locations/${regionId}/agents/${agentId}/sessions/${sessionId}:detectIntent`;``

## Request Structure

### 1. Dialogflow ES
| Entity | Content |
|--|--|
| Header | ``{ 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON }``|
| Body| ``{ event: content }`` <br>**or** <br> ``{ text: { languageCode: LanguageCode, text: content } }``|

### 2. Dialogflow CX
| Entity | Content |
|--|--|
| Header | ``{ 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON, 'Authorization': 'Bearer ' + accessToken }`` <br> |
| Body| ``{ event: { event: content} }`` <br> ``languageCode: LanguageCode`` <br>**or** <br>``{ text: { text: content } }`` <br> ``languageCode: LanguageCode`` |

## Response Structure

### 1. Dialogflow ES
| Parameters | Description |
|--|--|
| fulfillmentMessages | **Array:** Contains the text message and custom payloads of the agent response |
| isFallback | **Boolean:** Is true when intent is a fallback.|
| intent |**Object:** Triggered intent's name and display name.|
| intentDetectionConfidence |**Float:** Values: 0-1 |
| languageCode | **String:** Returns the language code of the agent's current language.|
| parameters | **Object:** The agent entities relative to the request |


### 2. Dialogflow CX
| Parameters | Description |
|--|--|
| currentPage | **Object:** The current page of the conversation flow. Contains name and displayName |
| diagnosticInfo | **Object:** Contains objects related to the flow and intent matching. <br> - Triggered Transistion Names <br> - Transistion Targets Chain <br> - Alternative Matched Intents|
| intent |**Object:** Triggered intent's name and display name as well as |
| intentDetectionConfidence |**Float:** Values: 0-1 |
| languageCode | **String:** Returns the language code of the agent's current language.|
| match | **Object:** Contains parameters related to the matched intents. <br> - confidence <br> - intent <br> - matchType <br> - resolvedInput|
| parameters | **Object:** The agent entities relative to the request |
| responseMessages | **Array:** Contains the text message, custom payloads of the agent response, successful response metadata, live agent handoff data and conditional response messages|

## Apps.Dialogflow Requirement Alterations

### 1. Fallbacks
Fallbacks are used by Apps.Dialogflow to automatically connect a guest to a live agent after a set amount of wrong inputs. Dialogflow ES allows the creation of fallback intents which return `isFallback = true` in the response object. Dialogflow CX pages / intents cannot be set as fallbacks.

#### Proposed Solution
Dialogflow CX by default starts with a `sys.no-match.default` event. The event handler will contain a custom payload ``{
"isFallback": true
}``. The function parsing CX responses in Apps.Dialogflow will search for a parameter `isFallback` in the payloads array instead of searching the full response object.

10 changes: 5 additions & 5 deletions endpoints/IncomingEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export class IncomingEndpoint extends ApiEndpoint {
this.app.getLogger().info(Logs.ENDPOINT_RECEIVED_REQUEST);

try {
await this.processRequest(read, modify, http, request.content);
await this.processRequest(read, modify, persis, http, 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, http: IHttp, endpointContent: IActionsEndpointContent) {
private async processRequest(read: IRead, modify: IModify, persistence: IPersistence, http: IHttp, endpointContent: IActionsEndpointContent) {

const { action, sessionId } = endpointContent;
if (!sessionId) { throw new Error(Logs.INVALID_SESSION_ID); }
Expand All @@ -51,13 +51,13 @@ export class IncomingEndpoint extends ApiEndpoint {
if (!event) { throw new Error(Logs.INVALID_EVENT_DATA); }

try {
const response: IDialogflowMessage = await Dialogflow.sendRequest(http, read, modify, sessionId, event, DialogflowRequestType.EVENT);
const response: IDialogflowMessage = await Dialogflow.sendRequest(http, read, modify, persistence, sessionId, event, DialogflowRequestType.EVENT);
const livechatRoom = await read.getRoomReader().getById(sessionId) as ILivechatRoom;
if (!livechatRoom) { throw new Error(); }
const { visitor: { token: vToken } } = livechatRoom;
await createDialogflowMessage(sessionId, read, modify, response);
this.app.getLogger().log(response)
await handlePayloadActions(read, modify, sessionId, vToken, response);
this.app.getLogger().log(response);
await handlePayloadActions(read, modify, http, persistence, sessionId, vToken, response);
} catch (error) {
this.app.getLogger().error(`${Logs.DIALOGFLOW_REST_API_ERROR} ${error.message}`);
throw new Error(`${Logs.DIALOGFLOW_REST_API_ERROR} ${error.message}`);
Expand Down
1 change: 1 addition & 0 deletions enum/ActionIds.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum ActionIds {
PERFORM_HANDOVER = 'df_perform_handover',
CLOSE_CHAT = 'df_close_chat',
SET_TIMEOUT = 'df_set_timeout',
}
4 changes: 3 additions & 1 deletion enum/Dialogflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface IDialogflowMessage {
messages?: Array<string | IDialogflowQuickReplies | IDialogflowPayload | IDialogflowImageCard>;
isFallback: boolean;
sessionId?: string;
parameters?: any;
}

export interface IDialogflowQuickReplies {
Expand Down Expand Up @@ -78,6 +79,7 @@ export enum Base64 {

export enum LanguageCode {
EN = 'en',
BR = 'pt-BR',
}

export enum DialogflowRequestType {
Expand All @@ -87,5 +89,5 @@ export enum DialogflowRequestType {

export enum Message {
CLOSED_BY_VISITOR = 'Closed by visitor',
CUSTOMER_IDEL_TIMEOUT = 'customer_idle_timeout'
CUSTOMER_IDEL_TIMEOUT = 'customer_idle_timeout',
}
2 changes: 1 addition & 1 deletion handler/OnAgentAssignedHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class OnAgentAssignedHandler {

try {
const event = { name: 'Welcome', languageCode: 'en', parameters: {...livechatData, roomId: rid, visitorToken} || {} };
const response: IDialogflowMessage = await Dialogflow.sendRequest(this.http, this.read, this.modify, rid, event, DialogflowRequestType.EVENT);
const response: IDialogflowMessage = await Dialogflow.sendRequest(this.http, this.read, this.modify, this.persis, rid, event, DialogflowRequestType.EVENT);

await createDialogflowMessage(rid, this.read, this.modify, response);
} catch (error) {
Expand Down
19 changes: 11 additions & 8 deletions handler/PostMessageSentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import { botTypingListener, removeBotTypingListener } from '../lib//BotTyping';
import { Dialogflow } from '../lib/Dialogflow';
import { createDialogflowMessage, createMessage } from '../lib/Message';
import { handlePayloadActions } from '../lib/payloadAction';
import { handleParameters } from '../lib/responseParameters';
import { closeChat, performHandover, updateRoomCustomFields } from '../lib/Room';
import { getAppSettingValue } from '../lib/Settings';
import { incFallbackIntentAndSendResponse, resetFallbackIntent } from '../lib/SynchronousHandover';
import { handleTimeout } from '../lib/Timeout';

export class PostMessageSentHandler {
constructor(private readonly app: IApp,
private readonly message: ILivechatMessage,
private readonly read: IRead,
private readonly http: IHttp,
private readonly persistence: IPersistence,
private readonly modify: IModify) { }
private readonly message: ILivechatMessage,
private readonly read: IRead,
private readonly http: IHttp,
private readonly persistence: IPersistence,
private readonly modify: IModify) { }

public async run() {
const { text, editedAt, room, token, sender, customFields } = this.message;
Expand Down Expand Up @@ -86,7 +87,7 @@ export class PostMessageSentHandler {

try {
await botTypingListener(rid, this.modify.getNotifier().typing({ id: rid, username: DialogflowBotUsername }));
response = (await Dialogflow.sendRequest(this.http, this.read, this.modify, rid, text, DialogflowRequestType.MESSAGE));
response = (await Dialogflow.sendRequest(this.http, this.read, this.modify, this.persistence, rid, text, DialogflowRequestType.MESSAGE));
} catch (error) {
this.app.getLogger().error(`${Logs.DIALOGFLOW_REST_API_ERROR} ${error.message}`);

Expand All @@ -103,7 +104,9 @@ export class PostMessageSentHandler {
return;
}

handlePayloadActions(this.read, this.modify, rid, visitorToken, response);
handlePayloadActions(this.read, this.modify, this.http, this.persistence, rid, visitorToken, response);

handleParameters(this.read, this.modify, this.persistence, this.http, rid, visitorToken, response);

const createResponseMessage = async () => await createDialogflowMessage(rid, this.read, this.modify, response);

Expand Down Expand Up @@ -147,7 +150,7 @@ export class PostMessageSentHandler {
if (DialogflowEnableChatClosedByVisitorEvent) {
try {
let res: IDialogflowMessage;
res = (await Dialogflow.sendRequest(this.http, this.read, this.modify, rid, {
res = (await Dialogflow.sendRequest(this.http, this.read, this.modify, this.persistence, rid, {
name: DialogflowChatClosedByVisitorEventName,
languageCode: LanguageCode.EN,
}, DialogflowRequestType.EVENT));
Expand Down
7 changes: 7 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
{
"bot_username": "Bot Username",
"agent_version": "Agent Version",
"dialogflow_bot_id": "Bot Id",
"dialogflow_bot_id_description": "Select the bot to use from the list of bots. Use comma to seperate values in respective fields",
"dialogflow_project_id": "Project Id",
"dialogflow_client_email": "Client Email",
"dialogflow_cx_agent_id": "[CX ONLY] Dialogflow Agent ID",
"dialogflow_cx_agent_id_desc": "1. Go to the Dialogflow CX console. \n 2. Select the project. \n 3. From the agent list find the right agent and click `⋮` . \n 4. Select copy name. \n 5. The agent name has the following format: \n 6. `projects/PROJECT_ID/locations/REGION_ID/agents/AGENT_ID`. \n 7. Copy `AGENT_ID` and paste it here.",
"dialogflow_cx_fallback_events": "[CX ONLY] Fallback Events",
"dialogflow_cx_fallback_events_desc": "Dialogflow CX does not support fallback events. Enter a list of event names that will be treated as fallbacks from the app.",
"dialogflow_cx_region": "[CX ONLY] Dialogflow Region",
"dialogflow_cx_region_desc": "1. Go to the Dialogflow CX console. \n 2. Select the project. \n 3. From the agent list find the right agent and click `⋮` . \n 4. Select copy name. \n 5. The agent name has the following format: \n 6. `projects/PROJECT_ID/locations/REGION_ID/agents/AGENT_ID`. \n 7. Copy `REGION_ID` and paste it here.",
"dialogflow_private_key": "Private Key",
"dialogflow_environment": "Environment",
"dialogflow_environment_description": "Name of the Dialogflow environment. Default environment is draft",
Expand Down
Loading