From 0c3ef59e30fe0499febffcf9dc7a87ffdcee7e1e Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Sat, 15 May 2021 15:49:39 +0300 Subject: [PATCH 01/11] Initial commit --- config/Settings.ts | 44 ++++++++++++++- docs/CXAgent.md | 43 +++++++++++++++ i18n/en.json | 7 +++ lib/Dialogflow.ts | 135 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 docs/CXAgent.md diff --git a/config/Settings.ts b/config/Settings.ts index 5399fa9..82b366c 100644 --- a/config/Settings.ts +++ b/config/Settings.ts @@ -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', @@ -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 { @@ -267,4 +270,43 @@ export const settings: Array = [ i18nDescription: 'dialogflow_log_level_description', required: false, }, + { + 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.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.DialogflowCXFallbackEvents, + public: true, + type: SettingType.STRING, + packageValue: 'NO_MATCH, NO_INPUT', + i18nLabel: 'dialogflow_cx_fallback_events', + i18nDescription: 'dialogflow_cx_fallback_events_desc', + required: false, + }, ]; diff --git a/docs/CXAgent.md b/docs/CXAgent.md new file mode 100644 index 0000000..915dcd0 --- /dev/null +++ b/docs/CXAgent.md @@ -0,0 +1,43 @@ +# Set Up For Dialogflow CX Agent + +## 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.
+ +- Generate a key with the services credentials from `Service Accounts -> Actions -> Manage keys -> Add Key -> Create new key` with JSON key type
+- 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.
+ + +### 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. + +
+ +## Section II: Create Agent + +- From the [Dialogflow CX platform](https://dialogflow.cloud.google.com/cx/projects) select the project and create a new agent. + +
+ +## Section III: Setting up Apps.Dialogflow + + +- Set agent version to **CX**.
+- From the [Dialogflow CX platform](https://dialogflow.cloud.google.com/cx/projects) select the project and from your agent list press and select **Copy Name**. +- The agent name has the following format:
+projects/PROJECT_ID/locations/REGION_ID/agents/AGENT_ID +- Paste **AGENT_ID** to **Dialogflow Agent ID** and **REDION ID** to **Dialogflow Region**.
+ + +
+ +## 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. + + diff --git a/i18n/en.json b/i18n/en.json index 223aacd..0bb5e86 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", diff --git a/lib/Dialogflow.ts b/lib/Dialogflow.ts index b1894d8..082a2f0 100644 --- a/lib/Dialogflow.ts +++ b/lib/Dialogflow.ts @@ -17,24 +17,51 @@ class DialogflowClass { modify: IModify, sessionId: string, request: IDialogflowEvent | string, - requestType: DialogflowRequestType): Promise { + requestType: DialogflowRequestType): Promise { + const dialogFlowVersion = await getAppSettingValue(read, AppSetting.DialogflowVersion); + const serverURL = await this.getServerURL(read, modify, http, sessionId); - const queryInput = { - ...requestType === DialogflowRequestType.EVENT && { event: request }, - ...requestType === DialogflowRequestType.MESSAGE && { text: { languageCode: LanguageCode.EN, text: request } }, - }; + if (dialogFlowVersion === 'CX') { + + const queryInput = { + ...requestType === DialogflowRequestType.EVENT && { event: { event: typeof request === 'string' ? request : request.name} }, + ...requestType === DialogflowRequestType.MESSAGE && { text: { text: request }}, + languageCode: 'en', + }; - const httpRequestContent: IHttpRequest = createHttpRequest( - { 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON }, - { queryInput }, - ); + const accessToken = await this.getAccessToken(read, modify, http, sessionId); + if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } - try { - const response = await http.post(serverURL, httpRequestContent); - return this.parseRequest(response.data); - } catch (error) { - throw new Error(`${ Logs.HTTP_REQUEST_ERROR }`); + const httpRequestContent: IHttpRequest = createHttpRequest( + { 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON, 'Authorization': 'Bearer ' + accessToken }, + { queryInput }, + ); + + try { + const response = await http.post(serverURL, httpRequestContent); + return await this.parseCXRequest(read, response.data); + } catch (error) { + throw new Error(`${ Logs.HTTP_REQUEST_ERROR }`); + } + } else { + + const queryInput = { + ...requestType === DialogflowRequestType.EVENT && { event: request }, + ...requestType === DialogflowRequestType.MESSAGE && { text: { languageCode: LanguageCode.EN, text: request } }, + }; + + const httpRequestContent: IHttpRequest = createHttpRequest( + { 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON}, + { queryInput }, + ); + + try { + const response = await http.post(serverURL, httpRequestContent); + return this.parseRequest(response.data); + } catch (error) { + throw new Error(`${ Logs.HTTP_REQUEST_ERROR }`); + } } } @@ -148,16 +175,96 @@ class DialogflowClass { } } + public async parseCXRequest(read: IRead, response: any): Promise { + if (!response) { throw new Error(Logs.INVALID_RESPONSE_FROM_DIALOGFLOW_CONTENT_UNDEFINED); } + + const { session, queryResult } = response; + if (queryResult) { + const { responseMessages, match: { matchType } } = queryResult; + const fallbackEvents: string = await getAppSettingValue(read, AppSetting.DialogflowCXFallbackEvents); + + // Check array of event names from app settings for fallbacks + const parsedMessage: IDialogflowMessage = { + isFallback: fallbackEvents.split(/[ ,]+/).includes(matchType) ? true : false, + }; + + const messages: Array = []; + // customFields should be sent as the response of last message on client side + const msgCustomFields: IDialogflowCustomFields = {}; + + responseMessages.forEach((message) => { + const { text, payload: { quickReplies = null, customFields = null, action = null } = {} } = message; + if (text) { + const { text: textMessageArray } = text; + messages.push({ text: textMessageArray[0] }); + } + if (quickReplies) { + const { options, imagecards } = quickReplies; + if (options || imagecards) { + messages.push(quickReplies); + } + } + if (customFields) { + msgCustomFields.disableInput = !!customFields.disableInput; + msgCustomFields.disableInputMessage = customFields.disableInputMessage; + msgCustomFields.displayTyping = customFields.displayTyping; + } + if (action) { + messages.push({action}); + } + }); + + if (Object.keys(msgCustomFields).length > 0) { + if (messages.length > 0) { + let lastObj = messages[messages.length - 1]; + lastObj = Object.assign(lastObj, { customFields: msgCustomFields }); + messages[messages.length - 1] = lastObj; + } else { + messages.push({ customFields: msgCustomFields }); + } + } + + if (messages.length > 0) { + parsedMessage.messages = messages; + } + + if (session) { + // "session" format -> projects/project-id/agent/sessions/session-id + const splittedText: Array = session.split('/'); + const sessionId: string = splittedText[splittedText.length - 1]; + if (sessionId) { + parsedMessage.sessionId = sessionId; + } + } + + return parsedMessage; + } else { + // some error occurred. Dialogflow's response has a error field containing more info abt error + throw Error(`An Error occurred while connecting to Dialogflow's REST API\ + Error Details:- + message:- ${response.error.message}\ + status:- ${response.error.message}\ + Try checking the google credentials in App Setting and your internet connection`); + } + } + private async getServerURL(read: IRead, modify: IModify, http: IHttp, sessionId: string) { const botId = await getAppSettingValue(read, AppSetting.DialogflowBotId); const projectIds = (await getAppSettingValue(read, AppSetting.DialogflowProjectId)).split(','); const projectId = projectIds.length >= botId ? projectIds[botId - 1] : projectIds[0]; const environments = (await getAppSettingValue(read, AppSetting.DialogflowEnvironment)).split(','); const environment = environments.length >= botId ? environments[botId - 1] : environments[0]; + const regionId = await getAppSettingValue(read, AppSetting.DialogflowRegion); + const agentId = await getAppSettingValue(read, AppSetting.DialogflowAgentId); + const dialogFlowVersion = await getAppSettingValue(read, AppSetting.DialogflowVersion); const accessToken = await this.getAccessToken(read, modify, http, sessionId); if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } + if (dialogFlowVersion === 'CX') { + return `https://${regionId}-dialogflow.googleapis.com/v3/projects/${projectId}/locations/${regionId}/agents/${agentId}/sessions/${sessionId}:detectIntent`; + } + return `https://dialogflow.googleapis.com/v2/projects/${projectId}/agent/environments/${environment || 'draft'}/users/-/sessions/${sessionId}:detectIntent?access_token=${accessToken}`; } From b94f712c69f18950f9f43612592b2b615125a66b Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Sat, 15 May 2021 16:00:54 +0300 Subject: [PATCH 02/11] Minor doc fixes --- docs/CXAgent.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/CXAgent.md b/docs/CXAgent.md index 915dcd0..6dd9f79 100644 --- a/docs/CXAgent.md +++ b/docs/CXAgent.md @@ -5,16 +5,23 @@ ### 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.
+ -- Generate a key with the services credentials from `Service Accounts -> Actions -> Manage keys -> Add Key -> Create new key` with JSON key type
+ +- Generate a key with the services credentials from `Service Accounts -> Actions -> Manage keys -> Add Key -> Create new key` with JSON key type
+ + + - 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.
+ +- 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.
+ ### 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. +- 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.
From dd31a104e8c933123d143da50621267fd336cce1 Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Tue, 25 May 2021 17:30:23 +0300 Subject: [PATCH 03/11] Changed UI Element order. Changed fulfilment check to response custom payload --- config/Settings.ts | 69 ++++++++++++++++++++-------------------------- lib/Dialogflow.ts | 14 ++++++---- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/config/Settings.ts b/config/Settings.ts index 82b366c..1703e43 100644 --- a/config/Settings.ts +++ b/config/Settings.ts @@ -41,6 +41,18 @@ export enum DefaultMessage { } export const settings: Array = [ + { + 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, @@ -92,6 +104,24 @@ export const settings: Array = [ 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, @@ -270,43 +300,4 @@ export const settings: Array = [ i18nDescription: 'dialogflow_log_level_description', required: false, }, - { - 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.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.DialogflowCXFallbackEvents, - public: true, - type: SettingType.STRING, - packageValue: 'NO_MATCH, NO_INPUT', - i18nLabel: 'dialogflow_cx_fallback_events', - i18nDescription: 'dialogflow_cx_fallback_events_desc', - required: false, - }, ]; diff --git a/lib/Dialogflow.ts b/lib/Dialogflow.ts index 082a2f0..f667fb6 100644 --- a/lib/Dialogflow.ts +++ b/lib/Dialogflow.ts @@ -181,11 +181,10 @@ class DialogflowClass { const { session, queryResult } = response; if (queryResult) { const { responseMessages, match: { matchType } } = queryResult; - const fallbackEvents: string = await getAppSettingValue(read, AppSetting.DialogflowCXFallbackEvents); // Check array of event names from app settings for fallbacks const parsedMessage: IDialogflowMessage = { - isFallback: fallbackEvents.split(/[ ,]+/).includes(matchType) ? true : false, + isFallback: false, }; const messages: Array = []; @@ -193,7 +192,7 @@ class DialogflowClass { const msgCustomFields: IDialogflowCustomFields = {}; responseMessages.forEach((message) => { - const { text, payload: { quickReplies = null, customFields = null, action = null } = {} } = message; + const { text, payload: { quickReplies = null, customFields = null, action = null, isFallback = false } = {} } = message; if (text) { const { text: textMessageArray } = text; messages.push({ text: textMessageArray[0] }); @@ -212,6 +211,9 @@ class DialogflowClass { if (action) { messages.push({action}); } + if (isFallback) { + parsedMessage.isFallback = isFallback; + } }); if (Object.keys(msgCustomFields).length > 0) { @@ -258,13 +260,13 @@ class DialogflowClass { const agentId = await getAppSettingValue(read, AppSetting.DialogflowAgentId); const dialogFlowVersion = await getAppSettingValue(read, AppSetting.DialogflowVersion); - const accessToken = await this.getAccessToken(read, modify, http, sessionId); - if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } if (dialogFlowVersion === 'CX') { return `https://${regionId}-dialogflow.googleapis.com/v3/projects/${projectId}/locations/${regionId}/agents/${agentId}/sessions/${sessionId}:detectIntent`; } - + + const accessToken = await this.getAccessToken(read, modify, http, sessionId); + if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } return `https://dialogflow.googleapis.com/v2/projects/${projectId}/agent/environments/${environment || 'draft'}/users/-/sessions/${sessionId}:detectIntent?access_token=${accessToken}`; } From 5f73448e4e0ed75e82781883769bc92c0dfb849e Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Wed, 26 May 2021 21:12:07 +0300 Subject: [PATCH 04/11] Added ES-CX comparison doc. Added BR languageCode --- docs/ES-CX comparison.md | 90 ++++++++++++++++++++++++++++++++++++++++ enum/Dialogflow.ts | 1 + 2 files changed, 91 insertions(+) create mode 100644 docs/ES-CX comparison.md diff --git a/docs/ES-CX comparison.md b/docs/ES-CX comparison.md new file mode 100644 index 0000000..e12260e --- /dev/null +++ b/docs/ES-CX comparison.md @@ -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 }``
**or**
``{ text: { languageCode: LanguageCode, text: content } }``| + + ### 2. Dialogflow CX +| Entity | Content | +|--|--| +| Header | ``{ 'Content-Type': Headers.CONTENT_TYPE_JSON, 'Accept': Headers.ACCEPT_JSON, 'Authorization': 'Bearer ' + accessToken }``
| +| Body| ``{ event: { event: content} }``
``languageCode: LanguageCode``
**or**
``{ text: { text: content } }``
``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.
- Triggered Transistion Names
- Transistion Targets Chain
- 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.
- confidence
- intent
- matchType
- 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. + diff --git a/enum/Dialogflow.ts b/enum/Dialogflow.ts index 57cd40f..b7d2c1f 100644 --- a/enum/Dialogflow.ts +++ b/enum/Dialogflow.ts @@ -78,6 +78,7 @@ export enum Base64 { export enum LanguageCode { EN = 'en', + BR = 'pt-BR' } export enum DialogflowRequestType { From 00db30b13a9ffda701b95b2194e6fd50d5e6c76c Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Tue, 1 Jun 2021 22:19:58 +0300 Subject: [PATCH 05/11] added change language functionality --- endpoints/IncomingEndpoint.ts | 6 +- enum/Dialogflow.ts | 1 + handler/OnAgentAssignedHandler.ts | 2 +- handler/PostMessageSentHandler.ts | 7 ++- lib/Dialogflow.ts | 21 +++++-- lib/responseParameters.ts | 56 +++++++++++++++++++ lib/retrieveDataByAssociation.ts | 14 +++++ .../SessionMaintenanceProcessor.ts | 2 +- 8 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 lib/responseParameters.ts create mode 100644 lib/retrieveDataByAssociation.ts diff --git a/endpoints/IncomingEndpoint.ts b/endpoints/IncomingEndpoint.ts index ad098bd..ca6d4f1 100644 --- a/endpoints/IncomingEndpoint.ts +++ b/endpoints/IncomingEndpoint.ts @@ -23,7 +23,7 @@ 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); @@ -31,7 +31,7 @@ export class IncomingEndpoint extends ApiEndpoint { } } - 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); } @@ -51,7 +51,7 @@ 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; diff --git a/enum/Dialogflow.ts b/enum/Dialogflow.ts index b7d2c1f..c0e1cad 100644 --- a/enum/Dialogflow.ts +++ b/enum/Dialogflow.ts @@ -4,6 +4,7 @@ export interface IDialogflowMessage { messages?: Array; isFallback: boolean; sessionId?: string; + parameters?: any; } export interface IDialogflowQuickReplies { diff --git a/handler/OnAgentAssignedHandler.ts b/handler/OnAgentAssignedHandler.ts index e651991..95e864a 100644 --- a/handler/OnAgentAssignedHandler.ts +++ b/handler/OnAgentAssignedHandler.ts @@ -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) { diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index adb4231..7df867d 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -10,6 +10,7 @@ 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'; @@ -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}`); @@ -105,6 +106,8 @@ export class PostMessageSentHandler { handlePayloadActions(this.read, this.modify, 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); // synchronous handover check @@ -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)); diff --git a/lib/Dialogflow.ts b/lib/Dialogflow.ts index f667fb6..9d02a4f 100644 --- a/lib/Dialogflow.ts +++ b/lib/Dialogflow.ts @@ -1,4 +1,5 @@ import { IHttp, IHttpRequest, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; import { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import { createSign } from 'crypto'; import { AppSetting } from '../config/Settings'; @@ -7,6 +8,7 @@ import { Headers } from '../enum/Http'; import { Logs } from '../enum/Logs'; import { base64urlEncode } from './Helper'; import { createHttpRequest } from './Http'; +import { retrieveDataByAssociation } from './retrieveDataByAssociation'; import { updateRoomCustomFields } from './Room'; import { getAppSettingValue } from './Settings'; @@ -15,6 +17,7 @@ class DialogflowClass { public async sendRequest(http: IHttp, read: IRead, modify: IModify, + persistence: IPersistence, sessionId: string, request: IDialogflowEvent | string, requestType: DialogflowRequestType): Promise { @@ -23,13 +26,17 @@ class DialogflowClass { const serverURL = await this.getServerURL(read, modify, http, sessionId); if (dialogFlowVersion === 'CX') { + + const assoc = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `SFLAIA-${sessionId}`); + const data = await retrieveDataByAssociation(read, assoc); const queryInput = { ...requestType === DialogflowRequestType.EVENT && { event: { event: typeof request === 'string' ? request : request.name} }, ...requestType === DialogflowRequestType.MESSAGE && { text: { text: request }}, - languageCode: 'en', + languageCode: data.custom_languageCode || LanguageCode.EN, }; + const accessToken = await this.getAccessToken(read, modify, http, sessionId); if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } @@ -106,6 +113,7 @@ class DialogflowClass { } public parseRequest(response: any): IDialogflowMessage { + console.log(response); if (!response) { throw new Error(Logs.INVALID_RESPONSE_FROM_DIALOGFLOW_CONTENT_UNDEFINED); } const { session, queryResult } = response; @@ -176,6 +184,7 @@ class DialogflowClass { } public async parseCXRequest(read: IRead, response: any): Promise { + console.log(response); if (!response) { throw new Error(Logs.INVALID_RESPONSE_FROM_DIALOGFLOW_CONTENT_UNDEFINED); } const { session, queryResult } = response; @@ -239,6 +248,8 @@ class DialogflowClass { } } + parsedMessage.parameters = queryResult.parameters; + return parsedMessage; } else { // some error occurred. Dialogflow's response has a error field containing more info abt error @@ -256,15 +267,17 @@ class DialogflowClass { const projectId = projectIds.length >= botId ? projectIds[botId - 1] : projectIds[0]; const environments = (await getAppSettingValue(read, AppSetting.DialogflowEnvironment)).split(','); const environment = environments.length >= botId ? environments[botId - 1] : environments[0]; - const regionId = await getAppSettingValue(read, AppSetting.DialogflowRegion); - const agentId = await getAppSettingValue(read, AppSetting.DialogflowAgentId); const dialogFlowVersion = await getAppSettingValue(read, AppSetting.DialogflowVersion); if (dialogFlowVersion === 'CX') { + + const regionId = await getAppSettingValue(read, AppSetting.DialogflowRegion); + const agentId = await getAppSettingValue(read, AppSetting.DialogflowAgentId); + return `https://${regionId}-dialogflow.googleapis.com/v3/projects/${projectId}/locations/${regionId}/agents/${agentId}/sessions/${sessionId}:detectIntent`; } - + const accessToken = await this.getAccessToken(read, modify, http, sessionId); if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } return `https://dialogflow.googleapis.com/v2/projects/${projectId}/agent/environments/${environment || 'draft'}/users/-/sessions/${sessionId}:detectIntent?access_token=${accessToken}`; diff --git a/lib/responseParameters.ts b/lib/responseParameters.ts new file mode 100644 index 0000000..2b4085c --- /dev/null +++ b/lib/responseParameters.ts @@ -0,0 +1,56 @@ +import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import { AppSetting, DefaultMessage } from '../config/Settings'; +import { ActionIds } from '../enum/ActionIds'; +import { DialogflowRequestType, IDialogflowAction, IDialogflowMessage, IDialogflowPayload} from '../enum/Dialogflow'; +import { Logs } from '../enum/Logs'; +import { closeChat, performHandover, updateRoomCustomFields } from '../lib/Room'; +import { getAppSettingValue } from '../lib/Settings'; +import { Dialogflow } from './Dialogflow'; +import { createDialogflowMessage, createMessage } from './Message'; +import { retrieveDataByAssociation } from './retrieveDataByAssociation'; + +export const handleParameters = async (read: IRead, modify: IModify, persistence: IPersistence, http: IHttp, rid: string, visitorToken: string, dialogflowMessage: IDialogflowMessage) => { + const { parameters = [] } = dialogflowMessage; + console.log(parameters); + + if (parameters.custom_languagecode) { + + const assoc = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `SFLAIA-${rid}`); + const association = await read.getPersistenceReader().readByAssociation(assoc); + + const data = await retrieveDataByAssociation(read, assoc); + + if (data && data.custom_languageCode) { + if (data.custom_languageCode !== parameters.custom_languagecode) { + await persistence.updateByAssociation(assoc, {'custom_languageCode': parameters.custom_languagecode}); + sendChangeLanguageEvent(read, modify, persistence, rid, http, parameters.custom_languagecode); + } + } + else { + await persistence.createWithAssociation({'custom_languageCode': parameters.custom_languagecode}, assoc); + sendChangeLanguageEvent(read, modify, persistence, rid, http, parameters.custom_languagecode); + } + + } +} + +const sendChangeLanguageEvent = async (read: IRead, modify: IModify, persis: IPersistence, rid: string, http: IHttp, languageCode: string) => { + try { + + const event = { name: 'ChangeLanguage', languageCode: languageCode, parameters: {} }; + const response: IDialogflowMessage = await Dialogflow.sendRequest(http, read, modify, persis, rid, event, DialogflowRequestType.EVENT); + + await createDialogflowMessage(rid, read, modify, response); + } catch (error) { + + const serviceUnavailable: string = await getAppSettingValue(read, AppSetting.DialogflowServiceUnavailableMessage); + + await createMessage(rid, + read, + modify, + { text: serviceUnavailable ? serviceUnavailable : DefaultMessage.DEFAULT_DialogflowServiceUnavailableMessage }); + + return; + } +} \ No newline at end of file diff --git a/lib/retrieveDataByAssociation.ts b/lib/retrieveDataByAssociation.ts new file mode 100644 index 0000000..a233b44 --- /dev/null +++ b/lib/retrieveDataByAssociation.ts @@ -0,0 +1,14 @@ +import { IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; + +export const retrieveDataByAssociation = async (read: IRead, assoc: RocketChatAssociationRecord) => { + + const association = await read.getPersistenceReader().readByAssociation(assoc); + + if (association.length > 0) { + return Object.assign.apply(Object, association); + } + + return {}; + +} \ No newline at end of file diff --git a/lib/sessionMaintenance/SessionMaintenanceProcessor.ts b/lib/sessionMaintenance/SessionMaintenanceProcessor.ts index a880cca..caaaacc 100644 --- a/lib/sessionMaintenance/SessionMaintenanceProcessor.ts +++ b/lib/sessionMaintenance/SessionMaintenanceProcessor.ts @@ -39,7 +39,7 @@ export class SessionMaintenanceProcessor implements IProcessor { name: sessionMaintenanceEventName, languageCode: LanguageCode.EN, }; - await Dialogflow.sendRequest(http, read, modify, jobContext.sessionId, eventData, DialogflowRequestType.EVENT); + await Dialogflow.sendRequest(http, read, modify, persis, jobContext.sessionId, eventData, DialogflowRequestType.EVENT); } catch (error) { // console.log(error); } From 521da9911201161f6424703a2d96a8fb6e455bb9 Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Wed, 2 Jun 2021 17:31:38 +0300 Subject: [PATCH 06/11] Fixed no message in fulfilment big --- endpoints/IncomingEndpoint.ts | 2 +- enum/ActionIds.ts | 1 + handler/PostMessageSentHandler.ts | 8 ++++- lib/Dialogflow.ts | 51 ++++++++++++++++--------------- lib/payloadAction.ts | 33 +++++++++++++++++--- lib/responseParameters.ts | 1 - 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/endpoints/IncomingEndpoint.ts b/endpoints/IncomingEndpoint.ts index ca6d4f1..2e0fbf4 100644 --- a/endpoints/IncomingEndpoint.ts +++ b/endpoints/IncomingEndpoint.ts @@ -57,7 +57,7 @@ export class IncomingEndpoint extends ApiEndpoint { const { visitor: { token: vToken } } = livechatRoom; await createDialogflowMessage(sessionId, read, modify, response); this.app.getLogger().log(response) - await handlePayloadActions(read, modify, sessionId, vToken, 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}`); diff --git a/enum/ActionIds.ts b/enum/ActionIds.ts index 022f9b3..32a7f36 100644 --- a/enum/ActionIds.ts +++ b/enum/ActionIds.ts @@ -1,4 +1,5 @@ export enum ActionIds { PERFORM_HANDOVER = 'df_perform_handover', CLOSE_CHAT = 'df_close_chat', + SET_TIMEOUT = 'df_set_timeout', } diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index 7df867d..3712b0a 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -88,6 +88,9 @@ 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, this.persistence, rid, text, DialogflowRequestType.MESSAGE)); + + console.log('response'); + console.log(response); } catch (error) { this.app.getLogger().error(`${Logs.DIALOGFLOW_REST_API_ERROR} ${error.message}`); @@ -104,7 +107,10 @@ export class PostMessageSentHandler { return; } - handlePayloadActions(this.read, this.modify, rid, visitorToken, response); + console.log('handlePayloadActions'); + handlePayloadActions(this.read, this.modify, this.http, this.persistence, rid, visitorToken, response); + + console.log('handlePayloadParameters'); handleParameters(this.read, this.modify, this.persistence, this.http, rid, visitorToken, response); diff --git a/lib/Dialogflow.ts b/lib/Dialogflow.ts index 9d02a4f..553d1e9 100644 --- a/lib/Dialogflow.ts +++ b/lib/Dialogflow.ts @@ -113,7 +113,6 @@ class DialogflowClass { } public parseRequest(response: any): IDialogflowMessage { - console.log(response); if (!response) { throw new Error(Logs.INVALID_RESPONSE_FROM_DIALOGFLOW_CONTENT_UNDEFINED); } const { session, queryResult } = response; @@ -184,10 +183,10 @@ class DialogflowClass { } public async parseCXRequest(read: IRead, response: any): Promise { - console.log(response); if (!response) { throw new Error(Logs.INVALID_RESPONSE_FROM_DIALOGFLOW_CONTENT_UNDEFINED); } const { session, queryResult } = response; + if (queryResult) { const { responseMessages, match: { matchType } } = queryResult; @@ -200,30 +199,32 @@ class DialogflowClass { // customFields should be sent as the response of last message on client side const msgCustomFields: IDialogflowCustomFields = {}; - responseMessages.forEach((message) => { - const { text, payload: { quickReplies = null, customFields = null, action = null, isFallback = false } = {} } = message; - if (text) { - const { text: textMessageArray } = text; - messages.push({ text: textMessageArray[0] }); - } - if (quickReplies) { - const { options, imagecards } = quickReplies; - if (options || imagecards) { - messages.push(quickReplies); + if (responseMessages) { + responseMessages.forEach((message) => { + const { text, payload: { quickReplies = null, customFields = null, action = null, isFallback = false } = {} } = message; + if (text) { + const { text: textMessageArray } = text; + messages.push({ text: textMessageArray[0] }); } - } - if (customFields) { - msgCustomFields.disableInput = !!customFields.disableInput; - msgCustomFields.disableInputMessage = customFields.disableInputMessage; - msgCustomFields.displayTyping = customFields.displayTyping; - } - if (action) { - messages.push({action}); - } - if (isFallback) { - parsedMessage.isFallback = isFallback; - } - }); + if (quickReplies) { + const { options, imagecards } = quickReplies; + if (options || imagecards) { + messages.push(quickReplies); + } + } + if (customFields) { + msgCustomFields.disableInput = !!customFields.disableInput; + msgCustomFields.disableInputMessage = customFields.disableInputMessage; + msgCustomFields.displayTyping = customFields.displayTyping; + } + if (action) { + messages.push({action}); + } + if (isFallback) { + parsedMessage.isFallback = isFallback; + } + }); + } if (Object.keys(msgCustomFields).length > 0) { if (messages.length > 0) { diff --git a/lib/payloadAction.ts b/lib/payloadAction.ts index 5375065..829a7bc 100644 --- a/lib/payloadAction.ts +++ b/lib/payloadAction.ts @@ -1,11 +1,13 @@ -import { IModify, IRead } from '@rocket.chat/apps-engine/definition/accessors'; -import { AppSetting } from '../config/Settings'; +import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { AppSetting, DefaultMessage } from '../config/Settings'; import { ActionIds } from '../enum/ActionIds'; -import { IDialogflowAction, IDialogflowMessage, IDialogflowPayload} from '../enum/Dialogflow'; +import { DialogflowRequestType, IDialogflowAction, IDialogflowMessage, IDialogflowPayload} from '../enum/Dialogflow'; import { closeChat, performHandover, updateRoomCustomFields } from '../lib/Room'; import { getAppSettingValue } from '../lib/Settings'; +import { Dialogflow } from './Dialogflow'; +import { createDialogflowMessage, createMessage } from './Message'; -export const handlePayloadActions = async (read: IRead, modify: IModify, rid: string, visitorToken: string, dialogflowMessage: IDialogflowMessage) => { +export const handlePayloadActions = async (read: IRead, modify: IModify, http: IHttp, persistence: IPersistence, rid: string, visitorToken: string, dialogflowMessage: IDialogflowMessage) => { const { messages = [] } = dialogflowMessage; for (const message of messages) { const { action = null } = message as IDialogflowPayload; @@ -33,6 +35,29 @@ export const handlePayloadActions = async (read: IRead, modify: IModify, rid: } else if (actionName === ActionIds.CLOSE_CHAT) { await closeChat(modify, read, rid); } + else if (actionName === ActionIds.SET_TIMEOUT) { + let n = setTimeout(async () => { + try { + const event = { name: params.eventName, languageCode: 'en', parameters: {} }; + const response: IDialogflowMessage = await Dialogflow.sendRequest(http, read, modify, persistence, rid, event, DialogflowRequestType.EVENT); + await createDialogflowMessage(rid, read, modify, response); + } + catch (error) { + + const serviceUnavailable: string = await getAppSettingValue(read, AppSetting.DialogflowServiceUnavailableMessage); + + await createMessage(rid, + read, + modify, + { text: serviceUnavailable ? serviceUnavailable : DefaultMessage.DEFAULT_DialogflowServiceUnavailableMessage }); + + return; + } + + + }, Number(params.time)); + + } } } } diff --git a/lib/responseParameters.ts b/lib/responseParameters.ts index 2b4085c..f149fdd 100644 --- a/lib/responseParameters.ts +++ b/lib/responseParameters.ts @@ -12,7 +12,6 @@ import { retrieveDataByAssociation } from './retrieveDataByAssociation'; export const handleParameters = async (read: IRead, modify: IModify, persistence: IPersistence, http: IHttp, rid: string, visitorToken: string, dialogflowMessage: IDialogflowMessage) => { const { parameters = [] } = dialogflowMessage; - console.log(parameters); if (parameters.custom_languagecode) { From c346aafb27dab68ff1dca3db1fca61d1bf9af798 Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Wed, 2 Jun 2021 18:33:17 +0300 Subject: [PATCH 07/11] Small code quality changes --- lib/responseParameters.ts | 5 +---- lib/retrieveDataByAssociation.ts | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/responseParameters.ts b/lib/responseParameters.ts index f149fdd..d70a228 100644 --- a/lib/responseParameters.ts +++ b/lib/responseParameters.ts @@ -1,10 +1,7 @@ import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; import { AppSetting, DefaultMessage } from '../config/Settings'; -import { ActionIds } from '../enum/ActionIds'; -import { DialogflowRequestType, IDialogflowAction, IDialogflowMessage, IDialogflowPayload} from '../enum/Dialogflow'; -import { Logs } from '../enum/Logs'; -import { closeChat, performHandover, updateRoomCustomFields } from '../lib/Room'; +import { DialogflowRequestType, IDialogflowMessage} from '../enum/Dialogflow'; import { getAppSettingValue } from '../lib/Settings'; import { Dialogflow } from './Dialogflow'; import { createDialogflowMessage, createMessage } from './Message'; diff --git a/lib/retrieveDataByAssociation.ts b/lib/retrieveDataByAssociation.ts index a233b44..1b763f6 100644 --- a/lib/retrieveDataByAssociation.ts +++ b/lib/retrieveDataByAssociation.ts @@ -1,5 +1,5 @@ -import { IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; -import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import { IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; export const retrieveDataByAssociation = async (read: IRead, assoc: RocketChatAssociationRecord) => { From 11161828a4f7b95aa6306232944f92f99c60cf0d Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Wed, 2 Jun 2021 20:02:27 +0300 Subject: [PATCH 08/11] Small code quality changes --- handler/PostMessageSentHandler.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index 3712b0a..55628bd 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -88,9 +88,6 @@ 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, this.persistence, rid, text, DialogflowRequestType.MESSAGE)); - - console.log('response'); - console.log(response); } catch (error) { this.app.getLogger().error(`${Logs.DIALOGFLOW_REST_API_ERROR} ${error.message}`); @@ -107,11 +104,8 @@ export class PostMessageSentHandler { return; } - console.log('handlePayloadActions'); handlePayloadActions(this.read, this.modify, this.http, this.persistence, rid, visitorToken, response); - console.log('handlePayloadParameters'); - handleParameters(this.read, this.modify, this.persistence, this.http, rid, visitorToken, response); const createResponseMessage = async () => await createDialogflowMessage(rid, this.read, this.modify, response); From 15a01bc55bca2690629458f7f161b5f42350e0d1 Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Mon, 7 Jun 2021 21:07:00 +0300 Subject: [PATCH 09/11] Fixed TSLint syntax errors --- config/Settings.ts | 6 +++--- enum/Dialogflow.ts | 4 ++-- lib/Dialogflow.ts | 6 ++---- lib/payloadAction.ts | 24 ++++++++++++++---------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/config/Settings.ts b/config/Settings.ts index 1703e43..854154b 100644 --- a/config/Settings.ts +++ b/config/Settings.ts @@ -25,9 +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' + DialogflowAgentId = 'dialogflow_cx_agent_id', + DialogflowRegion = 'dialogflow_cx_region', + DialogflowCXFallbackEvents = 'dialogflow_cx_fallback_events', } export enum DefaultMessage { diff --git a/enum/Dialogflow.ts b/enum/Dialogflow.ts index c0e1cad..e9c0a51 100644 --- a/enum/Dialogflow.ts +++ b/enum/Dialogflow.ts @@ -79,7 +79,7 @@ export enum Base64 { export enum LanguageCode { EN = 'en', - BR = 'pt-BR' + BR = 'pt-BR', } export enum DialogflowRequestType { @@ -89,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', } diff --git a/lib/Dialogflow.ts b/lib/Dialogflow.ts index 553d1e9..8a6f3be 100644 --- a/lib/Dialogflow.ts +++ b/lib/Dialogflow.ts @@ -26,7 +26,7 @@ class DialogflowClass { const serverURL = await this.getServerURL(read, modify, http, sessionId); if (dialogFlowVersion === 'CX') { - + const assoc = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, `SFLAIA-${sessionId}`); const data = await retrieveDataByAssociation(read, assoc); @@ -36,7 +36,6 @@ class DialogflowClass { languageCode: data.custom_languageCode || LanguageCode.EN, }; - const accessToken = await this.getAccessToken(read, modify, http, sessionId); if (!accessToken) { throw Error(Logs.ACCESS_TOKEN_ERROR); } @@ -224,7 +223,7 @@ class DialogflowClass { parsedMessage.isFallback = isFallback; } }); - } + } if (Object.keys(msgCustomFields).length > 0) { if (messages.length > 0) { @@ -270,7 +269,6 @@ class DialogflowClass { const environment = environments.length >= botId ? environments[botId - 1] : environments[0]; const dialogFlowVersion = await getAppSettingValue(read, AppSetting.DialogflowVersion); - if (dialogFlowVersion === 'CX') { const regionId = await getAppSettingValue(read, AppSetting.DialogflowRegion); diff --git a/lib/payloadAction.ts b/lib/payloadAction.ts index 829a7bc..9fb56a1 100644 --- a/lib/payloadAction.ts +++ b/lib/payloadAction.ts @@ -34,26 +34,30 @@ export const handlePayloadActions = async (read: IRead, modify: IModify, http: await performHandover(modify, read, rid, visitorToken, targetDepartment); } else if (actionName === ActionIds.CLOSE_CHAT) { await closeChat(modify, read, rid); - } - else if (actionName === ActionIds.SET_TIMEOUT) { - let n = setTimeout(async () => { + } else if (actionName === ActionIds.SET_TIMEOUT) { + + setTimeout(async () => { try { const event = { name: params.eventName, languageCode: 'en', parameters: {} }; - const response: IDialogflowMessage = await Dialogflow.sendRequest(http, read, modify, persistence, rid, event, DialogflowRequestType.EVENT); + const response: IDialogflowMessage = await Dialogflow.sendRequest(http, + read, + modify, + persistence, + rid, + event, + DialogflowRequestType.EVENT); await createDialogflowMessage(rid, read, modify, response); - } - catch (error) { + } catch (error) { const serviceUnavailable: string = await getAppSettingValue(read, AppSetting.DialogflowServiceUnavailableMessage); - + await createMessage(rid, read, modify, { text: serviceUnavailable ? serviceUnavailable : DefaultMessage.DEFAULT_DialogflowServiceUnavailableMessage }); - + return; } - }, Number(params.time)); @@ -61,4 +65,4 @@ export const handlePayloadActions = async (read: IRead, modify: IModify, http: } } } -} \ No newline at end of file +}; From 8b2c86dd10dd06ec11dbefd0043d09f74251e3ea Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Mon, 7 Jun 2021 21:59:10 +0300 Subject: [PATCH 10/11] Switched timeout function with processor. Fixed TSLint syntax errors --- DialogflowApp.ts | 5 +++- app.json | 5 ++++ config/Settings.ts | 2 +- endpoints/IncomingEndpoint.ts | 2 +- handler/PostMessageSentHandler.ts | 10 +++---- lib/EventTimeoutProcessor.ts | 17 ++++++++++++ lib/payloadAction.ts | 44 +++++++++++++++++-------------- lib/responseParameters.ts | 15 +++++------ lib/retrieveDataByAssociation.ts | 2 +- 9 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 lib/EventTimeoutProcessor.ts diff --git a/DialogflowApp.ts b/DialogflowApp.ts index a46924f..1397fa9 100644 --- a/DialogflowApp.ts +++ b/DialogflowApp.ts @@ -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'; @@ -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))); } diff --git a/app.json b/app.json index 40c53ee..4b85f54 100644 --- a/app.json +++ b/app.json @@ -18,5 +18,10 @@ "IPostLivechatAgentUnassigned", "IPostLivechatRoomClosed", "IUIKitLivechatInteractionHandler" + ], + "permissions": [ + { + "name": "scheduler" + } ] } \ No newline at end of file diff --git a/config/Settings.ts b/config/Settings.ts index 854154b..8581f8d 100644 --- a/config/Settings.ts +++ b/config/Settings.ts @@ -3,7 +3,7 @@ export enum AppSetting { DialogflowBotUsername = 'dialogflow_bot_username', DialogflowBotId = 'dialogflow_bot_id', DialogflowProjectId = 'dialogflow_project_id', - DialogflowVersion = "dialog_flow_version", + DialogflowVersion = 'dialog_flow_version', DialogflowClientEmail = 'dialogflow_client_email', DialogFlowPrivateKey = 'dialogflow_private_key', DialogflowEnvironment = 'dialogflow_environment', diff --git a/endpoints/IncomingEndpoint.ts b/endpoints/IncomingEndpoint.ts index 2e0fbf4..b14efb3 100644 --- a/endpoints/IncomingEndpoint.ts +++ b/endpoints/IncomingEndpoint.ts @@ -56,7 +56,7 @@ export class IncomingEndpoint extends ApiEndpoint { if (!livechatRoom) { throw new Error(); } const { visitor: { token: vToken } } = livechatRoom; await createDialogflowMessage(sessionId, read, modify, response); - this.app.getLogger().log(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}`); diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index 55628bd..07e252f 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -18,11 +18,11 @@ 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; diff --git a/lib/EventTimeoutProcessor.ts b/lib/EventTimeoutProcessor.ts new file mode 100644 index 0000000..21c80a3 --- /dev/null +++ b/lib/EventTimeoutProcessor.ts @@ -0,0 +1,17 @@ +import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { IJobContext, IProcessor } from '@rocket.chat/apps-engine/definition/scheduler'; +import { DialogflowRequestType } from '../enum/Dialogflow'; +import { createDialogflowMessage } from './Message'; + +export class EventScheduler implements IProcessor { + public id: string; + + constructor(id: string) { + this.id = id; + } + + public async processor(jobContext: IJobContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise { + await createDialogflowMessage(jobContext.rid, read, modify, jobContext.response); + return ; + } +} diff --git a/lib/payloadAction.ts b/lib/payloadAction.ts index 9fb56a1..9a4e1c2 100644 --- a/lib/payloadAction.ts +++ b/lib/payloadAction.ts @@ -36,30 +36,34 @@ export const handlePayloadActions = async (read: IRead, modify: IModify, http: await closeChat(modify, read, rid); } else if (actionName === ActionIds.SET_TIMEOUT) { - setTimeout(async () => { - try { - const event = { name: params.eventName, languageCode: 'en', parameters: {} }; - const response: IDialogflowMessage = await Dialogflow.sendRequest(http, - read, - modify, - persistence, - rid, - event, - DialogflowRequestType.EVENT); - await createDialogflowMessage(rid, read, modify, response); - } catch (error) { + const event = { name: params.eventName, languageCode: 'en', parameters: {} }; + const response: IDialogflowMessage = await Dialogflow.sendRequest(http, + read, + modify, + persistence, + rid, + event, + DialogflowRequestType.EVENT); - const serviceUnavailable: string = await getAppSettingValue(read, AppSetting.DialogflowServiceUnavailableMessage); + const task = { + id: 'event-scheduler', + when: `${Number(params.time)} seconds`, + data: {response, rid}, + }; - await createMessage(rid, - read, - modify, - { text: serviceUnavailable ? serviceUnavailable : DefaultMessage.DEFAULT_DialogflowServiceUnavailableMessage }); + try { + await modify.getScheduler().scheduleOnce(task); + } catch (error) { - return; - } + const serviceUnavailable: string = await getAppSettingValue(read, AppSetting.DialogflowServiceUnavailableMessage); + + await createMessage(rid, + read, + modify, + { text: serviceUnavailable ? serviceUnavailable : DefaultMessage.DEFAULT_DialogflowServiceUnavailableMessage }); - }, Number(params.time)); + return; + } } } diff --git a/lib/responseParameters.ts b/lib/responseParameters.ts index d70a228..58776d9 100644 --- a/lib/responseParameters.ts +++ b/lib/responseParameters.ts @@ -19,22 +19,21 @@ export const handleParameters = async (read: IRead, modify: IModify, persisten if (data && data.custom_languageCode) { if (data.custom_languageCode !== parameters.custom_languagecode) { - await persistence.updateByAssociation(assoc, {'custom_languageCode': parameters.custom_languagecode}); + await persistence.updateByAssociation(assoc, {custom_languageCode: parameters.custom_languagecode}); sendChangeLanguageEvent(read, modify, persistence, rid, http, parameters.custom_languagecode); } - } - else { - await persistence.createWithAssociation({'custom_languageCode': parameters.custom_languagecode}, assoc); + } else { + await persistence.createWithAssociation({custom_languageCode: parameters.custom_languagecode}, assoc); sendChangeLanguageEvent(read, modify, persistence, rid, http, parameters.custom_languagecode); } - } -} + } +}; const sendChangeLanguageEvent = async (read: IRead, modify: IModify, persis: IPersistence, rid: string, http: IHttp, languageCode: string) => { try { - const event = { name: 'ChangeLanguage', languageCode: languageCode, parameters: {} }; + const event = { name: 'ChangeLanguage', languageCode, parameters: {} }; const response: IDialogflowMessage = await Dialogflow.sendRequest(http, read, modify, persis, rid, event, DialogflowRequestType.EVENT); await createDialogflowMessage(rid, read, modify, response); @@ -49,4 +48,4 @@ const sendChangeLanguageEvent = async (read: IRead, modify: IModify, persis: IPe return; } -} \ No newline at end of file +}; diff --git a/lib/retrieveDataByAssociation.ts b/lib/retrieveDataByAssociation.ts index 1b763f6..67b268d 100644 --- a/lib/retrieveDataByAssociation.ts +++ b/lib/retrieveDataByAssociation.ts @@ -11,4 +11,4 @@ export const retrieveDataByAssociation = async (read: IRead, assoc: RocketChatA return {}; -} \ No newline at end of file +}; From 7dc2228f6ba7c7c5adecb1c87b8dd3b538f18dea Mon Sep 17 00:00:00 2001 From: AlexanderKanakis Date: Tue, 8 Jun 2021 14:34:08 +0300 Subject: [PATCH 11/11] removed permission system --- app.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app.json b/app.json index 4b85f54..40c53ee 100644 --- a/app.json +++ b/app.json @@ -18,10 +18,5 @@ "IPostLivechatAgentUnassigned", "IPostLivechatRoomClosed", "IUIKitLivechatInteractionHandler" - ], - "permissions": [ - { - "name": "scheduler" - } ] } \ No newline at end of file