From ec41162e36a6082bacad7eb51c80ad0e5032e78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Levasseur?= Date: Thu, 12 Mar 2026 11:44:11 -0400 Subject: [PATCH 1/2] feat(plugins/hitl,interfaces/hitl): allow linking messages + passing additional data (#15006) Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- interfaces/hitl/interface.definition.ts | 25 +++++++++++++------ plugins/hitl/plugin.definition.ts | 16 ++++++++++++ plugins/hitl/src/conv-manager.ts | 14 +++++------ .../src/hooks/before-incoming-message/all.ts | 20 ++++++++++++--- plugins/hitl/src/types.ts | 4 +++ 5 files changed, 61 insertions(+), 18 deletions(-) diff --git a/interfaces/hitl/interface.definition.ts b/interfaces/hitl/interface.definition.ts index ddf7aafd89f..0c9eaf41861 100644 --- a/interfaces/hitl/interface.definition.ts +++ b/interfaces/hitl/interface.definition.ts @@ -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.' + ), }), }) @@ -33,7 +42,7 @@ const messageSchema = sdk.z.union(messagePayloadSchemas as Tuple { + public async respond(messagePayload: types.MessagePayload, tags: types.MessageTags = {}): Promise { // 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 @@ -99,7 +99,7 @@ export class ConversationManager { type: messagePayload.type, userId: this._props.ctx.botId, payload: messagePayload, - tags: {}, + tags, }) break case 'image': @@ -107,7 +107,7 @@ export class ConversationManager { type: messagePayload.type, userId: this._props.ctx.botId, payload: messagePayload, - tags: {}, + tags, }) break case 'audio': @@ -115,7 +115,7 @@ export class ConversationManager { type: messagePayload.type, userId: this._props.ctx.botId, payload: messagePayload, - tags: {}, + tags, }) break case 'file': @@ -123,7 +123,7 @@ export class ConversationManager { type: messagePayload.type, userId: this._props.ctx.botId, payload: messagePayload, - tags: {}, + tags, }) break case 'video': @@ -131,7 +131,7 @@ export class ConversationManager { type: messagePayload.type, userId: this._props.ctx.botId, payload: messagePayload, - tags: {}, + tags, }) break case 'bloc': @@ -139,7 +139,7 @@ export class ConversationManager { type: messagePayload.type, userId: this._props.ctx.botId, payload: messagePayload, - tags: {}, + tags, }) break default: diff --git a/plugins/hitl/src/hooks/before-incoming-message/all.ts b/plugins/hitl/src/hooks/before-incoming-message/all.ts index ba188aa2bbd..7448d217ef5 100644 --- a/plugins/hitl/src/hooks/before-incoming-message/all.ts +++ b/plugins/hitl/src/hooks/before-incoming-message/all.ts @@ -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 } @@ -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 @@ -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 } @@ -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 } diff --git a/plugins/hitl/src/types.ts b/plugins/hitl/src/types.ts index 795c285ecca..b15820b2e30 100644 --- a/plugins/hitl/src/types.ts +++ b/plugins/hitl/src/types.ts @@ -21,6 +21,10 @@ export type MessagePayload = { } & sdk.z.infer }[keyof BaseMessagePayloads] +export type MessageTags = { + [T in keyof bp.message.Message['tags']]?: string +} + export type ActionableConversation = NonNullable< Awaited> > From ffb4dd39e44e20305d12b2f3f53f938d4c24190e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Levasseur?= Date: Thu, 12 Mar 2026 13:35:10 -0400 Subject: [PATCH 2/2] fix(plugins/hitl): revert changes (#15007) --- plugins/hitl/src/conv-manager.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/hitl/src/conv-manager.ts b/plugins/hitl/src/conv-manager.ts index 2d5da6ba01e..780028de352 100644 --- a/plugins/hitl/src/conv-manager.ts +++ b/plugins/hitl/src/conv-manager.ts @@ -85,7 +85,7 @@ export class ConversationManager { await this.respond({ type: 'text', text }) } - public async respond(messagePayload: types.MessagePayload, tags: types.MessageTags = {}): Promise { + public async respond(messagePayload: types.MessagePayload, _tags: types.MessageTags = {}): Promise { // 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 @@ -93,6 +93,9 @@ export class ConversationManager { // FIXME: typescript has trouble narrowing the type here, so we use a switch // statement as a workaround. + + const tags: Record = {} // re-enable tags, when the new hub allows updating or upgrading plugins + switch (messagePayload.type) { case 'text': await this._conversation.createMessage({