Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions interfaces/hitl/interface.definition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import * as sdk from '@botpress/sdk'

const MAX_ADDITIONAL_DATA_SIZE = 500 // corresponds to max message tag size in the runtime API

type AnyMessageType = { schema: sdk.z.ZodObject }
const withUserId = (s: AnyMessageType) => ({
const withHitlSpecific = (s: AnyMessageType) => ({
...s,
schema: () =>
s.schema.extend({
userId: sdk.z.string().optional().describe('Allows sending a message pretending to be a certain user'),
additionalData: sdk.z
.string()
.max(MAX_ADDITIONAL_DATA_SIZE)
.optional()
.describe(
'Additional data to send with the message, useful for custom integrations. Must be encoded in a string.'
),
}),
})

Expand Down Expand Up @@ -33,7 +42,7 @@ const messageSchema = sdk.z.union(messagePayloadSchemas as Tuple<sdk.AnyZodObjec

export default new sdk.InterfaceDefinition({
name: 'hitl',
version: '2.0.0',
version: '2.1.0',
entities: {
hitlSession: {
title: 'HITL session',
Expand Down Expand Up @@ -178,12 +187,12 @@ export default new sdk.InterfaceDefinition({
channels: {
hitl: {
messages: {
text: withUserId(sdk.messages.defaults.text),
image: withUserId(sdk.messages.defaults.image),
audio: withUserId(sdk.messages.defaults.audio),
video: withUserId(sdk.messages.defaults.video),
file: withUserId(sdk.messages.defaults.file),
bloc: withUserId(sdk.messages.markdownBloc), // TODO: use the actual bloc message when bumping a version of the interface
text: withHitlSpecific(sdk.messages.defaults.text),
image: withHitlSpecific(sdk.messages.defaults.image),
audio: withHitlSpecific(sdk.messages.defaults.audio),
video: withHitlSpecific(sdk.messages.defaults.video),
file: withHitlSpecific(sdk.messages.defaults.file),
bloc: withHitlSpecific(sdk.messages.markdownBloc), // TODO: use the actual bloc message when bumping a version of the interface
},
},
},
Expand Down
16 changes: 16 additions & 0 deletions plugins/hitl/plugin.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,22 @@ export default new sdk.PluginDefinition({
},
},
},
message: {
tags: {
downstream: {
title: 'Downstream Message ID',
description: 'ID of the downstream message bound to the upstream one',
},
upstream: {
title: 'Upstream Message ID',
description: 'ID of the upstream message bound to the downstream one',
},
additionalData: {
title: 'Additional Data',
description: 'Additional data optionally sent with the message by the downstream integration',
},
},
},
interfaces: {
hitl: sdk.version.allWithinMajorOf(hitl),
},
Expand Down
17 changes: 10 additions & 7 deletions plugins/hitl/src/conv-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,61 +85,64 @@ export class ConversationManager {
await this.respond({ type: 'text', text })
}

public async respond(messagePayload: types.MessagePayload): Promise<void> {
public async respond(messagePayload: types.MessagePayload, _tags: types.MessageTags = {}): Promise<void> {
// FIXME: in the future, we should use the provided UserId so that messages
// on Botpress appear to come from the agent/user instead of the
// bot user. For now, this is not possible because of checks in the
// backend.

// FIXME: typescript has trouble narrowing the type here, so we use a switch
// statement as a workaround.

const tags: Record<string, string> = {} // re-enable tags, when the new hub allows updating or upgrading plugins

switch (messagePayload.type) {
case 'text':
await this._conversation.createMessage({
type: messagePayload.type,
userId: this._props.ctx.botId,
payload: messagePayload,
tags: {},
tags,
})
break
case 'image':
await this._conversation.createMessage({
type: messagePayload.type,
userId: this._props.ctx.botId,
payload: messagePayload,
tags: {},
tags,
})
break
case 'audio':
await this._conversation.createMessage({
type: messagePayload.type,
userId: this._props.ctx.botId,
payload: messagePayload,
tags: {},
tags,
})
break
case 'file':
await this._conversation.createMessage({
type: messagePayload.type,
userId: this._props.ctx.botId,
payload: messagePayload,
tags: {},
tags,
})
break
case 'video':
await this._conversation.createMessage({
type: messagePayload.type,
userId: this._props.ctx.botId,
payload: messagePayload,
tags: {},
tags,
})
break
case 'bloc':
await this._conversation.createMessage({
type: messagePayload.type,
userId: this._props.ctx.botId,
payload: messagePayload,
tags: {},
tags,
})
break
default:
Expand Down
20 changes: 17 additions & 3 deletions plugins/hitl/src/hooks/before-incoming-message/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ const _handleDownstreamMessage = async (
})
}

await upstreamCm.respond({ ...messagePayload, userId: upstreamUserId })
await upstreamCm.respond(
{ ...messagePayload, userId: upstreamUserId },
{ downstream: props.data.id, additionalData: _getMessageAdditionalData(props.data) }
)
return consts.STOP_EVENT_HANDLING
}

Expand All @@ -90,6 +93,15 @@ const _getMessagePayloadIfSupported = (msg: client.Message): types.MessagePayloa
? ({ type: msg.type, ...msg.payload } as types.MessagePayload)
: undefined

const _getMessageAdditionalData = (msg: client.Message): string | undefined => {
// should be typed by the SDK because it's part of the hitl interface
const propName = 'additionalData'
if (propName in msg.payload && typeof msg.payload[propName] === 'string') {
return msg.payload[propName]
}
return
}

const _handleUpstreamMessage = async (
props: bp.HookHandlerProps['before_incoming_message'],
upstreamConversation: types.ActionableConversation
Expand All @@ -109,9 +121,11 @@ const _handleUpstreamMessage = async (

if (!messagePayload) {
props.logger.with(props.data).error('Upstream conversation received a non-text message')

const supportedMessageTypes = consts.SUPPORTED_MESSAGE_TYPES.join(', ')
await upstreamCm.respond({
type: 'text',
text: 'Sorry, I can only handle text messages for now. Please try again.',
text: `Sorry, I can only handle one of the following message types: ${supportedMessageTypes}`,
})
return consts.STOP_EVENT_HANDLING
}
Expand Down Expand Up @@ -153,7 +167,7 @@ const _handleUpstreamMessage = async (
}

props.logger.withConversationId(upstreamConversation.id).info('Sending message to downstream')
await downstreamCm.respond({ ...messagePayload, userId: patientDownstreamUserId })
await downstreamCm.respond({ ...messagePayload, userId: patientDownstreamUserId }, { upstream: props.data.id })

return consts.STOP_EVENT_HANDLING
}
Expand Down
4 changes: 4 additions & 0 deletions plugins/hitl/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export type MessagePayload = {
} & sdk.z.infer<BaseMessagePayloads[TMsgType]['schema']>
}[keyof BaseMessagePayloads]

export type MessageTags = {
[T in keyof bp.message.Message['tags']]?: string
}

export type ActionableConversation = NonNullable<
Awaited<ReturnType<bp.ActionHandlerProps['conversations']['hitl']['hitl']['getById']>>
>
Expand Down
Loading