Integrating Oracle Digital Assistant (ODA) with an Agent System
This project allows ODA (19.1.3 and above) to handover a user chat to a third party Agent Handover system other than Oracle Service Cloud. Project is shipped with mock agent server 'with no UI' that prints out user messages to console. Also a sample implementation to hand over chat to "Oracle Engagement Cloud (19A or above)".
Features
This project extends ODA out of the box features integrating with Oracle Service Cloud to different agent systems, so you will still use the out of the box System.AgentInitiation and System.AgentConversation components; hence your skill is totally abstracted from back-end agent system details. By using ODA built-in system components, the following features are supported:
- Passing user conversation history to agent upon initiating a chat request.
- Bot user can terminate chat conversation at anytime using the exit keywords specified in System.AgentConversation component.
- Upon terminating a chat conversation, Agent can redirect the bot to a specific state as specified in the actions property in System.AgentInitiation component.
- Upon terminating a chat conversation, Agent can decide to automatically add a new utterance to an intent.
High level architecture
As described in the below screenshot, this integration uses ODA webhook channel to pass user messages to webhook implementation, webhook implementation calls out your custom agent implementation file that is basically responsible for message transformations between ODA and agent system, and then message sent to agent or ODA depending from where the message originated.
Install
- Clone project repository
git clone git@github.com:oracle/cloud-asset-oda-agent-handover.git
- Navigate to project directory
cd cloud-asset-oda-agent-handover
and install required librariesnpm install
- Edit
cloud-asset-oda-agent-handover/config/config.js
and update ODA webhook details as specified in section Configure ODA Skill. Also you need to update AGENT_IMPL_FILE, For more details check Integrate with a third party agent system
// Digital Assistance Webhook channel details
module.exports.DA_WEBHOOK_URL = "ODA_WEBHOOK_URL";
module.exports.DA_WEBHOOK_SECRET = "ODA_WEBHOOK_SECRET";
// Webhook Implementation
module.exports.PORT = process.env.PORT || 4444;
/**
* Name of the agent implementation file without the extension.
* This file exists under oda_agent_handover/src/agentImpl folder
*/
module.exports.AGENT_IMPL_FILE = "mockAgent";
- Run webhook using
npm start
ornpm run start:dev
the later will start server in development mode with code live reload enabled using nodemon and inspect mode on port 9229 for debugging. - If you make code changes, you can run
npm run lint:fix
to lint your code using eslint. - If you are running locally on your laptop, you need to expose your webhook to the internet through an SSL connection. For development purposes, you can using ngrok
ngrok http 4444
Configure ODA Skill
- Create a new Skill for example AgentFramework and replace the generated BOTML with the following sample:
metadata:
platformVersion: "1.1"
main: true
name: AgentFramework
context:
variables:
question: "string"
myCustomProps: "string"
states:
setUserEmail:
component: "System.SetVariable"
properties:
value: "CHANGE_TO_YOUR_EMAIL"
variable: "profile.email"
transitions: {}
setUserFirstName:
component: "System.SetVariable"
properties:
value: "CHANGE_TO_YOUR_FIRST_NAME"
variable: "profile.firstName"
transitions: {}
setUserLastName:
component: "System.SetVariable"
properties:
value: "CHANGE_TO_YOUR_LAST_NAME"
variable: "profile.lastName"
transitions: {}
setCustomProperties:
component: "System.SetVariable"
properties:
variable: "myCustomProps"
value:
Product:
- Name: "Product X"
Serial: "CUSTOM_PRODUCT_SERIAL_NUMBER"
Country:
- City: "MY_CITY"
Code: "MY_COUNTRY"
Profile:
- mobileNumber: "CHANGE_TO_YOUR_MOBILE_NUMBER"
userName: "CHANGE_TO_ANY_USER_NAME"
start:
component: "System.Text"
properties:
prompt: "How can I help you?"
variable: "question"
startContactAgent:
component: "System.AgentInitiation"
properties:
subject: "${question}"
agentChannel: "AgentFrameworkChannel"
agentActions: "MyAction,Payments"
waitingMessage: "Chat accepted, waiting for agent to join"
rejectedMessage: "Apologies no agent is available at the moment"
customProperties: "${myCustomProps.value}"
transitions:
actions:
accepted: "agentConversation"
rejected: "rejected"
agentConversation:
component: "System.AgentConversation"
properties:
agentChannel: "AgentFrameworkChannel"
exitKeywords: "bye,thanks,thank you"
conclusionMessage: "Agent left the conversation, we will redirect you back again to our automated bot."
transitions:
actions:
MyAction: "agentRedirection"
Payments: "agentRedirection"
next: "exit"
agentRedirection:
component: "System.Output"
properties:
text: "Agent left conversation and redirected you to this state."
keepTurn: false
transitions:
next: "exit"
rejected:
component: "System.Output"
properties:
text: "Agent Rejected chat request."
keepTurn: true
exit:
component: "System.Output"
properties:
text: "End of Agent Handover demo."
transitions:
return: "start"
- Create a new channel and point it to your webhook implementation URL
https://ADDRESS:PORT/bot/message
If you are using ngrok to expose your webhook implementation, then the that URL would behttps://NGROK_URL/bot/message
- As mentioned in step (3) in the installation section, update the ODA_WEBHOOK_URL and ODA_WEBHOOK_SECRET to map to the details of the newly created channel.
- Route new channel to your skill.
- Note down the channel name, and then back to your skill BOTML, update the System.AgentInitiation component in state startContactAgent and make sure that agentChannel property is set to the name of your channel you created. Apply the same for the System.AgentConversation component in state agentConversation
startContactAgent:
component: "System.AgentInitiation"
properties:
subject: "${question}"
agentChannel: "AgentFrameworkChannel"
agentActions: "MyAction,Payments"
waitingMessage: "Chat accepted, waiting for agent to join"
rejectedMessage: "Apologies no agent is available at the moment"
customProperties: "${myCustomProps.value}"
transitions:
actions:
accepted: "agentConversation"
rejected: "rejected"
agentConversation:
component: "System.AgentConversation"
properties:
agentChannel: "AgentFrameworkChannel"
exitKeywords: "bye,thanks,thank you"
conclusionMessage: "Agent left the conversation, we will redirect you back again to our automated bot."
transitions:
actions:
MyAction: "agentRedirection"
Payments: "agentRedirection"
next: "exit"
- To ensure conversation history is passed along, make sure to enable conversation logging.
Payloads
Below are the details of the message formats exchanged between the webhook implementation and agent server.
From Webhook to Agent
All messages sent from Webhook to agent system will pass a botUser object. This identifies the user conversation session in ODA. Agent system should store this object and send it back along will all messages to webhook. ODA will use this value to properly route messages to the correct user conversation session.
-
Request chat: sent when a user request chat with an agent.
{ "botUser": { "userId": "3281467" }, "conversationHistory": [ { "payload": "start", "id": "5d59878d-b451-4dca-b348-bab296a8d896", "source": "USER", "createdOn": 1552039641121 }, { "payload": "How can I help you?", "id": "ba910a5a-74f9-41bb-8b31-222c3384de5d", "source": "BOT", "createdOn": 1552039641205 }, { "payload": "I want to speak with an agent", "id": "407aa4b5-7c40-4ff0-a1b4-3ec5d24e7cd6", "source": "USER", "createdOn": 1552039647643 } ], "actions": [ { "action": "MyAction", "description": "MyAction", "label": "MyAction" }, { "action": "Payments", "description": "Payments", "label": "Payments" } ], "firstName": "CHANGE_TO_YOUR_FIRST_NAME", "lastName": "CHANGE_TO_YOUR_LAST_NAME", "email": "CHANGE_TO_YOUR_EMAIL", "message": "I want to speak with an agent", "metadata": { "Country": [ { "City": "MyCity", "Code": "MyCountry" } ], "MyProduct": [ { "Serial": "CUSTOM_PRODUCT_SERIAL_NUMBER", "Name": "Product X" } ], "Profile": [ { "mobileNumber": "CHANGE_TO_YOUR_MOBILE_NUMBER", "userName": "CHANGE_TO_ANY_USER_NAME" } ] } }
conversationHistory: an array containing the full conversation history between user and bot. It is the agent system responsibility to parse it and outputs a proper interface in agent system.
actions: an array of action objects passed to agent. An agent can use the the value of action.action property to terminate chat with user and redirect bot to a specific state in BOTML. For more details, check section Agent Action Transition.
metadata: a JSON object holding custom properties, this maps to the customProperties property in System.AgentInitiation state. Basically this is any custom metadata that can be passed along chatRequest.
-
Send Chat Message: user sends a chat message to agent during an ongoing conversation.
{ "botUser": { "userId": "3246969" }, "message": "This is a message from bot to agent", "sessionId": "123456" }
sessionId: a unique value that identifies user chat session in agent system. This is a value passed from agent system upon accepting a chat request. For more details, check section Chat Accepted.
-
Terminate Chat: user terminates chat conversation with an agent by sending any of the exit key words defined in System.AgentConversation component in stat agentConversation
{ "botUser": { "userId": "3246969" }, "message": "terminate chat from bot", "sessionId": "123456" }
sessionId: a unique value that identifies user chat session in agent system. This is a value passed from agent system upon accepting a chat request. For more details, check section Chat Accepted.
From Agent to Webhook
Agent back-end system, must POST responses to webhook implementation on https://WEBHOOK_ADDRESS:PORT/agent/message
All messages sent from Agent system to webhook will pass a sessionId property. This identifies the user conversation session in agent system. Webhook implementation will store this property and send it back along will all messages to agent system. Agent system will use this property to properly route messages to the correct user conversation session.
-
Request chat Accepted: sent when agent accepts a user chat request.
{ "type": "accepted", "payload": { "message": "Hello my name is Agent, how can I help you?", "sessionId": "123456", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
-
Request chat Delayed: sent when when user chat request is pending in queue waiting for an agent to pick it up.
{ "type": "delayed", "payload": { "message": "Our agents are busy serving others, your waiting time is 15 mins", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
-
Request chat Rejected: sent when agent reject a user chat request.
{ "type": "rejected", "payload": { "message": "sorry, you contacted us out of office hours", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
-
Send Chat message: agent sends a chat message to user during an ongoing conversation.
{ "type": "agent", "payload": { "message": "Hello my name is Agent, how can I help you?", "sessionId": "123456", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
-
Terminate Chat: agent terminates chat conversation with a user.
{ "type": "agentLeft", "payload": { "message": "Thanks for contacting us, you will be redirected back to bot", "sessionId": "123456", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
-
Agent Action Transition: agent terminates chat conversation with a user and redirects bot to a specific state as specified in the action property. For more details check section Request Chat.
{ "type": "agentAction", "payload": { "action": "Payments", "sessionId": "123456", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
-
Agent Action Learn: agent terminates chat conversation with a user and add an utterance to an intent. Format is
learn INTENT_NAME, SAMPLE_UTTERANCE
{ "type": "agentAction", "payload": { "action": "learn Payment, pay my bills", "sessionId": "123456", "botUser": { "userId": "3246969" } } }
botUser: the user ID in ODA, for more details check section From Webhook to Agent.
Start Agent Mock Server
By default, project is configured with a mock agent server that outputs messages to console. To start server run npm run mock-server
this starts a nodeJS server on port 4445. To use a different agent system, follow the steps in section Integrate with a third party agent system
Using Agent Mocker Server
-
Follow steps in Install section
-
Follow steps in Configure ODA Skill section
-
Run mock server using
npm run mock-server
-
Start a conversation from ODA skill tester.
-
Upon receiving a chat request, mock-server console will outputs the following message
Received chat request, payload is: { botUser: { userId: '9289908' }, conversationHistory: [ { payload: 'start', id: 'b29e4050-0fe2-4067-9747-51492bb22113', source: 'USER', createdOn: 1552232161095 }, { payload: 'How can I help you?', id: '2168b148-07a9-4cf5-9b5a-10741e6da97e', source: 'BOT', createdOn: 1552232161183 }, { payload: 'I want to speak with an agent', id: '3fb63125-b912-4f18-98b8-5c4231f497fb', source: 'USER', createdOn: 1552232164203 } ], actions: [ { action: 'MyAction', description: 'MyAction', label: 'MyAction' }, { action: 'Payments', description: 'Payments', label: 'Payments' } ], firstName: 'CHANGE_TO_YOUR_FIRST_NAME', lastName: 'CHANGE_TO_YOUR_LAST_NAME', email: 'CHANGE_TO_YOUR_EMAIL', message: 'I want to speak with an agent' }
-
POST a ChatAccepted or ChatDelayed response as specified in From Agent to Webhook section make sure to copy botUser object from mock server console and use it within your payload. Note that a dummy sessionId is used with the value of 12345.
curl -X POST \ http://localhost:4444/agent/message \ -H 'Content-Type: application/json' \ -d '{ "type": "accepted", "payload": { "message": "Hello my name is Agent, how can I help you?", "sessionId": "123456", "botUser": { "userId": "9289908" } } }'
-
After accepting a chat request you can send any of the supported payloads as specified in section From Agent to Webhook.
Integrate with a third party agent system
By default, project is configured to work with a sample agent mock server. Both mock server and webhook implementation assumes the the following payloads to be exchanged. In reality, it is not always possible to customize the backend agent system to accommodate these formats. Hence you need to do message transformation in between.
To add a new implementation to your agent system, you need to create an implementation file under cloud-asset-oda-agent-handover\src\agentImpl
for example MyAgentImpl.js
and then specify the name of that file in the config file as mentioned in the installation section. If you created your agent implementation file inside a new folder for example cloud-asset-oda-agent-handover\src\agentImpl\myFolder\MyAgentImpl.js
, then you need to specify the name of that folder too, for example module.exports.AGENT_IMPL_FILE = "/myFolder/MyAgentImpl";
. Your agent implementation must conforms with a specific structure,a sample is shipped in same folder mockAgent.js
.
// Agent API Base URL
const AGENT_API_BASE_URL = "http://localhost:4445/agent/api/chat/v1/";
const log4js = require("log4js");
const logger = log4js.getLogger("AgentImpl");
logger.level = "debug";
class MockAgent {
constructor() {
}
/**
* Transform a message payload from webhook structure to Agent structure.
* @returns {object} agent message
* @param {object} payload : Message Payload to send to agent, you need to transform to agent message payload.
* @param {string} payloadType : Flag to indicate the payload type. Allowed values are requestChat|concludeChat|postMessage
*/
async toAgentPayloadStructure(payload, payloadType) {
try {
switch (payloadType) {
case "requestChat":
{
// Transform payload to agent requestChat payload
break;
}
case "concludeChat":
{
// Transform payload to agent terminateChat payload
break;
}
case "postMessage":
{
// Transform payload to agent postChat payload
break;
}
}
return payload;
} catch (error) {
logger.error("Error transforming payload from ODA format to agent format, detailed error: %s", error.message);
throw new Error(error);
}
}
/**
* Transform a message payload from Agent structure to webhook payload structure.
* @returns {object} ODA Message payload
* @param {string} payload : Agent message payload to transform to ODA format
*/
async fromAgentPayloadStructure(payload) {
try {
return payload;
} catch (error) {
logger.error("Error transforming payload from agent format to ODA format, detailed error: %s", error.message);
throw new Error(error);
}
}
/**
* Build an axios config body as documented in https://github.com/axios/axios that will be used for the REST call to agent API.
* @returns {object} an axios config body.
* @param {object} payload : Payload to send to agent API.
* @param {string} payloadType : Flag to indicate the payload type. Allowed values are requestChat|concludeChat|postMessage. payloadType can be used to determine the appropriate Agent API endpoint to call.
*/
async buildRestCallPayload(payload, payloadType) {
try {
let request = {
method: "POST", // HTTP method
data: payload, // method body
url: AGENT_API_BASE_URL + payloadType,
responseType: "json",
// validateStatus: function (status) {
// return (status >= 200 && status < 300) // Default
// },
// headers: { // Agent API headers
// "X-Custom-Header": "foobar",
// "X-Custom-Header2": "foobar2",
// },
// auth: {
// username: "AgentUser", // Agent API userName
// password: "mypassword" // Agent API Password
// },
// proxy: {
// host: "127.0.0.1", // Proxy Server Address
// port: 8080, // Proxy Server Port
// auth: {
// username: "PROXY_USER", // Proxy Server UserName
// password: "myPassword" // Proxy Server Password
// }
// }
};
return request;
} catch (error) {
logger.error("Error building rest call payload to agent, detailed error: %s", error.message);
throw new Error(error);
}
}
/**
* Result of calling "buildRestCallPayload" are passed to this method
* @param {object} payload : The payload sent by "buildRestCallPayload" method to Agent.
* @param {object} result : The result of calling "buildRestCallPayload" method if any.
* @param {string} payloadType : Flag to indicate the payload type. Allowed values are requestChat|concludeChat|postMessage. payloadType can be used to determine the appropriate Agent API endpoint to call.
* @param {number} statusCode: HTTP Response Code for calling the REST end point
*/
async restCallResult(payload, result, payloadType, statusCode) {
try {
// Do something with result if needed
} catch (error) {
logger.error("Error processing agent rest call result, detailed error: %s", error.message);
throw new Error(error);
}
}
}
module.exports = MockAgent;
-
toAgentPayloadStructure: in this method you convert payloads from the default webhook implementation structure as specified in section From Webhook to agent to agent specific payload structure. The method is passed two parameters, the actual payload and a payloadType.
- payload is one of the payloads defined in section From Webhook to agent
- payloadType is a string value indicating the type of payload, it is one of the following values requestChat, concludeChat, postMessage. You use this value as a control flag to differentiate between the different payloads generated from webhook and then convert to the appropriate corresponding agent payload structure. Method must return the transformed payload.
-
fromAgentPayloadStructure: in this method you convert payloads from agent structure to webhook implementation structure as specified in section From Agent to Webhook. This method is passed a payload property which is the raw JSON object as received from agent post back call. It is the the developer responsibility to understand the payload structures sent from agent system and differentiate between them and then transform to the corresponding structure as specified in section From Agent to Webhook. Method must return the transformed payload.
-
buildRestCallPayload: This method generates the axios config body to execute. Webhook implementation uses axios as an HTTP client to invoke REST calls on agent system. The method is passed two parameters, the transformed payload and a payloadType.
- payload is the transformed payload generated by toAgentPayloadStructure method.
- payloadType is a string value indicating the type of payload, it is one of the following values requestChat, concludeChat, postMessage. If agent system exposes different endpoints to handle different actions -for example the endpoint to receive a chatRequest is different from the endpoint to receive a chatMessage - you can use the payloadType property to differentiate between the payloads and consequently call the correct endpoint.
If your agent system requires authentication, specific header params or behind a proxy, you can un-comment the corresponding properties and update as needed. Note that an axios config body supports many other properties, for complete list visit axios documentation page on github
-
restCallResult: This method is passed the result of calling buildRestCallPayload
- payload is the transformed payload generated by toAgentPayloadStructure method and sent to to agent.
- result is the result of sending the payload to agent.
- payloadType is a string value indicating the type of payload, it is one of the following values requestChat, concludeChat, postMessage. If agent system exposes different endpoints to handle different actions -for example the endpoint to receive a chatRequest is different from the endpoint to receive a chatMessage - you can use the payloadType property to differentiate between the payloads and consequently do different actions.
Integrate with Oracle Engagement Cloud (19A or above)
-
Edit
cloud-asset-oda-agent-handover/config/config.js
and set the value of module.exports.AGENT_IMPL_FILE to engagementCloud/** * Name of the agent implementation file without the extension. * This file exists under oda_agent_handover/src/agentImpl folder */ module.exports.AGENT_IMPL_FILE = "/engagementCloud/engagementCloud";
-
Edit
cloud-asset-oda-agent-handover/src/agentImpl/engagementCloud/ecUtils.js
and change values as needed. Note that you need to properly secure this file as it will contains your instance username/password.// EC Base URL, for example https://myEcInstance.mydomain.com module.exports.EC_URI = "https://myEcInstance.mydomain.com"; // FA user Credentials (service user) that has access to "EC chat consumer APIs" module.exports.CREDENTIALS_FA_SERVICE_USER = "USER_NAME"; module.exports.CREDENTIALS_FA_SERVICE_USER_PASSWORD = "PASSWORD"; // Default Authenticate Chat properties, for details check section (3. Authenticate explained here https://docs.oracle.com/en/cloud/saas/engagement/19b/facoe/c_chat_quick_start.html) // Note: You can only (optionally) change the below values. module.exports.CHAT_AUTHENTICATE_INTERFACE_ID = 1; module.exports.CHAT_AUTHENTICATE_QUEUE_ID = 1; module.exports.CHAT_AUTHENTICATE_PRODUCT_ID = null; module.exports.CHAT_AUTHENTICATE_INCIDENT_ID = null; module.exports.CHAT_AUTHENTICATE_INCIDENT_TYPE = null; module.exports.CHAT_AUTHENTICATE_RESUME_TYPE = "RESUME"; module.exports.CHAT_AUTHENTICATE_MEDIA_LIST = "CHAT"; // Default Polling time (in milliseconds) to get new messages from Engagement Cloud. Default value is 3 seconds // convert to milliseconds * seconds module.exports.CHAT_LISTENER_POL_INTERVAL = 1000 * 3;
-
run webhook by executing
npm start
Contributing
cloud-asset-oda-agent-handover
is an open source project. See CONTRIBUTING for details.
Oracle gratefully acknowledges the contributions to cloud-asset-oda-agent-handover
that have been made by the community.
Considerations
This is project is intended to be a sample and not an official extension to be used in production systems. For example, the implementation uses an in-memory cache to store metadata that is used by the implementation. It is possible to use an external full fledged database, for that you need to customize cloud-asset-oda-agent-handover/src/lib/userStore/impl/UserStore.js
. However for the sake of simplicity and demo purposes, an in-memory cache store is used.