Implementing Human in the loop (HITL) in an integration
The Human in the loop (HITL) interface allows you to implement human agent intervention in your integration.
Terminology
Throughout this document, we will use the following terms:
Integration: The code that connects Botpress to an external service.
External service: The service that provides HITL functionality. This could be a help desk system like Zendesk, or any other system that allows human agents to send and receive messages to end users.
Human agent: A person who interacts with end users through the external service. This could be a support agent, a sales representative, or any other type of human agent.
End user: A person who interacts with your bot through Botpress. This could be a customer, a user, an employee, or any other type of end user.
External user: A representation of an end user within the external service. This is typically created when a HITL session is started.
HITL session: A conversation between an end user and a human agent. This is typically represented as a ticket in the external service.
HITL interface: The interface that defines the contract for implementing HITL functionality in your integration. This interface specifies the actions, events, and channels that your integration must implement to support HITL.
HITL plugin: The Botpress plugin that manages HITL sessions and relays messages between end users and your integration. Installing this plugin in a bot enables HITL functionality using the selected integration.
Entity: In the context of the HITL interface and plugin, an entity can be provided in order to support extra parameters in the startHitl action. This is useful to provide extra information about the HITL session, such as a ticket priority or customer identification number.
External service requirements
The external service providing HITL functionality must support the following:
- An API that allows creating external users.
- An API that allows creating HITL sessions.
- An API that allows adding messages to HITL sessions.
- An API that allows closing HITL sessions.
- Webhooks that can notify your integration of the following events:
- HITL session closure.
- Human agent assignment.
- Human agent reply.
Updating your package.json file
Finding the current interface version
The current version of the hitl interface is: 2.0.0
You will need this version number for the next steps.
Adding the interface as a dependency
Once you have the HITL interface version, you can add it as a dependency to your integration:
Add the dependencies section
If there is no bpDependencies section in your integration’s package.json file, create one:
{
"bpDependencies": {}
} Add the interface as a dependency
In the bpDependencies section, add the HITL interface as a dependency. For example, for version 2.0.0, you would add the following:
{
"bpDependencies": {
"hitl": "interface:hitl@2.0.0"
}
}It’s very important to follow this syntax:
"<interface-name>": "interface:<interface-name>@<version>".
Install the interface
Now that you have added the HITL interface as a dependency, you can run the bp add command to install it. This command will:
- Download the interface from Botpress.
- Install it in a directory named
bp_modulesin your integration’s root directory.
Adding a helper build script
To keep your integration up to date, we recommend adding a helper build script to your package.json file:
Now, whenever you run npm run build, it will automatically install the HITL interface and build your integration.
Editing your integration definition file
Adding the interface to your integration definition file
Now that the HITL interface is installed, you must add it your integration definition file in order to implement it.
Import the interface
At the top of the file, import the HITL interface:
import hitl from './bp_modules/hitl' Add an empty entity
In your new IntegrationDefinition() statement, add an empty entity:
export default new sdk.IntegrationDefinition({
entities: {
ticket: { // <= name of the entity
schema: sdk.z.object({})
},
},
})The name of the entity isn’t important, but it’s recommended to use a name that matches the terminology of the external service. For example, if the external service uses the term “ticket” to refer to a HITL session, you could name the entity ticket or hitlTicket.
Extend your definition
Use the .extend() function at the end of your new IntegrationDefinition() statement:
export default new sdk.IntegrationDefinition({
entities: {
ticket: { // <= name of the entity
schema: sdk.z.object({})
},
},
})
.extend(hitl, (self) => ({
entities: {
hitlSession: self.entities.ticket, // <= name of the entity
},
}))The exact syntax of .extend() will be explained in the next section.
Configuring the interface
The .extend() function takes two arguments:
- The first argument is a reference to the interface you want to implement. In this case, it’s
hitl. - The second argument is a configuration object. Using this object, you can override interface defaults with custom names, titles, and descriptions.
Whilst renaming actions, events and channels is optional, it’s highly recommended to rename these to match the terminology of the external service. This will help you avoid confusion and make your integration easier to understand.
Renaming actions
The hitl interface defines three actions that are used to interact with the external service:
createUser- Used by the HITL plugin to request the creation of a user in the external service and on Botpress.startHitl- Used by the HITL plugin to request the creation of a HITL session in the external service.stopHitl- Used by the HITL plugin to request the closure of a HITL session in the external service.
If you want to rename these actions, you can do so in the configuration object. For example, if you want to rename createUser to hitlCreateUser, you can do it like this:
.extend(hitl, () => ({
actions: {
createUser: {
name: 'hitlCreateUser',
},
},
}))
For example, if you’re using a help desk system like Zendesk, Jira Service Desk, or Freshdesk for HITL functionality, you might rename startHitl to createTicket and stopHitl to closeTicket. These systems use tickets to represent help requests, so renaming actions to match their terminology makes your integration clearer and easier to understand.
Renaming events
The hitl interface defines these events to notify the plugin of changes in the external service:
hitlAssigned- Emitted by your integration to notify the HITL plugin that a human agent has been assigned to a HITL session.hitlStopped- Emitted by your integration to notify the HITL plugin that a HITL session has been closed.
If you want to rename these events, you can do so in the configuration object. For example, if you want to rename hitlAssigned to agentAssigned, you can do it like this:
.extend(hitl, () => ({
events: {
hitlAssigned: {
name: 'agentAssigned',
},
},
}))
Renaming channels
The hitl interface defines these channels:
hitl- Used by the HITL plugin to send and receive messages from the external service. This represents the communication channel for the HITL session, like a support ticket on Zendesk or a direct message thread on Slack.
If you want to rename this channel, you can do so in the configuration object. For example, if you want to rename hitl to supportTicket, you can do it like this:
.extend(hitl, () => ({
channels: {
hitl: {
name: 'supportTicket',
},
},
}))
Implementing the interface
Implementing the actions
Implementing createUser
The createUser action is used by the HITL plugin to request the creation of an external user (a requester) in the external service.
If you opted to rename the action to something else than createUser in the “Configuring the interface” section,
please use the new name instead of createUser.
Please refer to the expected input and output schemas for the action: interface.definition.ts line 85.
This action should implement the following logic:
Create a Botpress user
Create a Botpress user using the Botpress client by calling the client.createUser() method.
Create an external user
Create an external user on the external service using the external service’s API or SDK.
Map the external user to the Botpress user
Update the external user on the external service to map it to the Botpress user. Please refer to the external service’s documentation to know how to set extra metadata for the external user. The integration must be able at any time to query the external service in order to retrieve the Botpress user ID from the external user.
As reference, here’s how this logic is implemented in the Zendesk integration:
export default new bp.Integration({
actions: {
async createUser({ ctx, input, client }) {
// Create a Botpress user:
const { name, email, pictureUrl } = input
const { user } = await client.createUser({
name,
pictureUrl,
tags: {
email,
role: 'end-user',
},
})
// Create an external user on Zendesk:
const zendeskClient = getZendeskClient(ctx.configuration)
const zendeskUser = await zendeskClient.createOrUpdateUser({
role: 'end-user',
external_id: user.id, // <= map to the Botpress user ID
name,
email,
remote_photo_url: pictureUrl,
})
// Map the Botpress user to the external user:
await client.updateUser({
id: user.id,
tags: {
id: zendeskUser.id.toString(), // <= map to the external user ID
},
})
// Yield control back to the plugin and return the user ID:
return {
userId: user.id, // <= return the Botpress user ID
}
},
},
})
Implementing startHitl
The startHitl action is used by the HITL plugin to request the creation of a HITL session (typically a ticket) in the external service.
If you opted to rename the action to something else than startHitl in the “Configuring the interface” section,
please use the new name instead of startHitl.
Please refer to the expected input and output schemas for the action: interface.definition.ts line 109.
This action should implement the following logic:
Fetch the Botpress user
Fetch the Botpress user with ID input.userId that was passed in the input parameters.
Create a Botpress conversation
Create a Botpress conversation using the Botpress client by calling the client.getOrCreateConversation() method.
Create the HITL session
On the external service, create the HITL session. This is typically represented as a ticket in the external service.
Map the Botpress conversation to the HITL session
Update the Botpress conversation to map it to the HITL session. This is typically achieved by setting a ticketId tag on the Botpress conversation.
Map the HITL session to the Botpress conversation
Update the HITL session on the external service to map it to the Botpress conversation. Please refer to the external service’s documentation to know how to set extra metadata for the HITL session (typically a ticket). The integration must be able at any time to query the external service in order to retrieve the Botpress conversation ID from the HITL session.
As reference, here’s how this logic is implemented in the Zendesk integration:
export default new bp.Integration({
actions: {
async startHitl({ ctx, input, client }) {
// Fetch the Botpress user that was passed in the input parameters:
const { user } = await client.getUser({
id: input.userId,
})
// From the user's tags, retrieve the external user's id:
const zendeskAuthorId = user.tags.id
if (!zendeskAuthorId) {
throw new sdk.RuntimeError(`User ${user.id} isn't linked to a Zendesk user`)
}
// Create a new ticket on Zendesk:
const zendeskClient = getZendeskClient(ctx.configuration)
const ticketTitle = input.title ?? 'Untitled Ticket'
const ticketBody = 'A user created a support ticket'
const createdZendeskTicket = await zendeskClient.createTicket(ticketTitle, ticketBody, {
id: zendeskAuthorId, // <= map the ticket to the external user ID
})
// Create a Botpress conversation and map it to the Zendesk ticket:
const { conversation } = await client.getOrCreateConversation({
channel: 'hitl',
tags: {
id: createdZendeskTicket.id.toString(), // <= map to the ticket ID
},
})
// Map the Zendesk ticket to the Botpress conversation:
await zendeskClient.updateTicket(createdZendeskTicket.id, {
external_id: conversation.id, // <= map to the Botpress conversation ID
})
// Yield control back to the plugin and return the conversation ID:
return {
conversationId: conversation.id, // <= return the Botpress conversation ID
}
},
},
})
Relaying the conversation history
The input parameters of the startHitl action contain a messageHistory parameter. This parameter contains the conversation history that should be relayed to the external service to provide the human agent with context about the conversation. This parameter is an array of every message that was sent in the conversation prior to the HITL session being started.
type MessageHistory = Array<
| TextMessage
| ImageMessage
| AudioMessage
| VideoMessage
| FileMessage
| LocationMessage
| CarouselMessage
| CardMessage
| DropdownMessage
| ChoiceMessage
| BlocMessage
| MarkdownMessage
>
type Source =
| {
type: 'user'
userId: string
}
| {
type: 'bot'
}
interface TextMessage {
source: Source
type: 'text'
payload: {
text: string
}
}
interface ImageMessage {
source: Source
type: 'image'
payload: {
imageUrl: string
}
}
interface AudioMessage {
source: Source
type: 'audio'
payload: {
audioUrl: string
}
}
interface VideoMessage {
source: Source
type: 'video'
payload: {
videoUrl: string
}
}
interface FileMessage {
source: Source
type: 'file'
payload: {
fileUrl: string
title?: string
}
}
interface LocationMessage {
source: Source
type: 'location'
payload: {
latitude: number
longitude: number
address?: string
title?: string
}
}
interface CarouselMessage {
source: Source
type: 'carousel'
payload: {
items: Array<{
title: string
subtitle?: string
imageUrl?: string
actions: Array<{
action: 'postback' | 'url' | 'say'
label: string
value: string
}>
}>
}
}
interface CardMessage {
source: Source
type: 'card'
payload: {
title: string
subtitle?: string
imageUrl?: string
actions: Array<{
action: 'postback' | 'url' | 'say'
label: string
value: string
}>
}
}
interface DropdownMessage {
source: Source
type: 'dropdown'
payload: {
text: string
options: Array<{
label: string
value: string
}>
}
}
interface ChoiceMessage {
source: Source
type: 'choice'
payload: {
text: string
options: Array<{
label: string
value: string
}>
}
}
interface BlocMessage {
source: Source
type: 'bloc'
payload: {
items: Array<
BlocTextItem | BlocMarkdownItem | BlocImageItem | BlocAudioItem | BlocVideoItem | BlocFileItem | BlocLocationItem
>
}
}
interface BlocTextItem {
type: 'text'
payload: {
text: string
}
}
interface BlocMarkdownItem {
type: 'markdown'
payload: {
markdown: string
}
}
interface BlocImageItem {
type: 'image'
payload: {
imageUrl: string
}
}
interface BlocAudioItem {
type: 'audio'
payload: {
audioUrl: string
}
}
interface BlocVideoItem {
type: 'video'
payload: {
videoUrl: string
}
}
interface BlocFileItem {
type: 'file'
payload: {
fileUrl: string
title?: string
}
}
interface BlocLocationItem {
type: 'location'
payload: {
latitude: number
longitude: number
address?: string
title?: string
}
}
interface MarkdownMessage {
source: Source
type: 'markdown'
payload: {
markdown: string
}
} If you decide to relay the conversation history to the external service, you can do so by iterating over the messageHistory array and sending each message to the external service using its API or SDK. However, doing so might cause a significant number of notifications being sent to the external service. To alleviate this, you can choose to send only the last few messages in the conversation history, or to concatenate the messages into a single message. For example you could combine messages like this:
## User1 said:
> Hello, I need help with my order.
## Bot replied:
> I have escalated this conversation to a human agent. Please wait while I connect you.
Adding extra parameters to the startHitl action
If the external service requires extra parameters when starting a HITL session, you can add them to the hitlSession entity, or whichever name you chose for the entity in the Adding the interface to your integration definition file section. For example, if the external service requires a priority parameter, you can add it like this:
export default new sdk.IntegrationDefinition({
entities: {
ticket: {
// <= name of your entity
schema: sdk.z.object({
priority: sdk.z.string().optional(), // <= add the extra parameter here
}),
},
},
})
Doing so will display the priority parameter in the “Start HITL” card within the Botpress Studio, allowing the bot authors to set it. The value of this parameter will then be passed to the startHitl action as part of the hitlSession input parameter:
export default new bp.Integration({
actions: {
async startHitl({ ctx, input, client }) {
// Retrieve the extra parameter:
const priority = input.hitlSession?.priority ?? 'normal'
// Then use it to create the HITL session:
const createdZendeskTicket = await zendeskClient.createTicket(ticketTitle, ticketBody, {
priority, // <= pass the extra parameter to the external service
})
},
},
})
Implementing stopHitl
The stopHitl action is used by the HITL plugin to request the closure of a HITL session (typically a ticket) in the external service.
If you opted to rename the action to something else than stopHitl in the “Configuring the interface” section, please
use the new name instead of stopHitl.
Please refer to the expected input and output schemas for the action: interface.definition.ts line 162.
This action should implement the following logic:
Fetch the Botpress conversation
Fetch the Botpress conversation with ID input.conversationId that was passed in the input parameters.
Retrieve the HITL session's ID
From the Botpress conversation’s tags, retrieve the HITL session’s ID.
The input parameters contain an unused reason parameter. Please ignore it. This parameter will be removed in future
versions of the HITL interface.
As reference, here’s how this logic is implemented in the Zendesk integration:
export default new bp.Integration({
actions: {
async stopHitl({ ctx, input, client }) {
// Fetch the Botpress conversation that was passed in the input parameters:
const { conversation } = await client.getConversation({
id: input.conversationId,
})
// From the conversation's tags, retrieve the Zendesk ticket's id:
const ticketId: string | undefined = conversation.tags.id
if (!ticketId) {
return {}
}
const zendeskClient = getZendeskClient(ctx.configuration)
// Close the ticket on Zendesk:
await zendeskClient.updateTicket(ticketId, {
status: 'closed',
})
// Yield control back to the plugin:
return {}
},
},
})
Implementing the channel
The hitl channel is used by the HITL plugin relay end user messages to the HITL session, which is usually a ticket or thread in the external service.
If you opted to rename the channel to something else than hitl in the “Configuring the interface” section, please
use the new name instead of hitl.
This channel handler should implement the following logic:
Retrieve the HITL session's ID
From the Botpress conversation’s tags, retrieve the HITL session’s ID.
Retrieve the external user's ID
- If the payload contains a
userIdparameter, the message has been sent by the end user. Retrieve the external user’s ID from the tags of the Botpress userpayload.userId. - If the payload doesn’t contain a
userIdparameter, the message has been sent by the bot. Retrieve the external user’s ID from the tags of the attached Botpress user.
As reference, here’s how this logic is implemented in the Zendesk integration:
export default new bp.Integration({
channels: {
hitl: {
messages: {
async text({ client, conversation, ctx, payload, user }) {
// Retrieve the ticket id from the conversation's tags:
const zendeskTicketId = conversation.tags.id
// Retrieve the external user:
let zendeskAuthorId = user.tags.id
if (payload.userId) {
const { user: botpressUser } = await client.getUser({
id: payload.userId,
})
zendeskAuthorId = botpressUser.tags.id
}
// Send the message to Zendesk:
return await getZendeskClient(ctx.configuration).createComment(zendeskTicketId, zendeskAuthorId, payload.text)
},
},
},
},
})
Implementing the events
You should set up webhooks so that the integration receives notifications about these events:
- A new message is added to the HITL session (usually a ticket).
- A human agent has been assigned to the HITL session.
- The HITL session was closed.
Incoming messages
When notified by the external service that a new message has been added to the HITL session, you should relay the message to the Botpress conversation:
Implementing hitlAssigned
When notified by the external service that a human agent has been assigned to the HITL session, you should notify the HITL plugin by emitting the hitlAssigned event:
If you opted to rename the event to something else than hitlAssigned in the “Configuring the interface” section,
please use the new name instead of hitlAssigned.
Implementing hitlStopped
When notified by the external service that the HITL session was closed, you should notify the HITL plugin by emitting the hitlStopped event:
If you opted to rename the event to something else than hitlStopped in the “Configuring the interface” section,
please use the new name instead of hitlStopped.