diff --git a/bots/bugbuster/src/handlers/message-created.ts b/bots/bugbuster/src/handlers/message-created.ts index b282629795b..e2335450be6 100644 --- a/bots/bugbuster/src/handlers/message-created.ts +++ b/bots/bugbuster/src/handlers/message-created.ts @@ -18,7 +18,10 @@ export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => { props.logger.info(`Ignoring message from ${conversation.integration}`) return } - if (conversation.integration === 'slack' && conversation.channel === 'channel') { + if ( + conversation.integration === 'slack' && + (conversation.channel === 'channel' || conversation.channel === 'thread') + ) { return } diff --git a/integrations/notion/definitions/secrets.ts b/integrations/notion/definitions/secrets.ts index 708d9e4aef7..eb6dd5f44c4 100644 --- a/integrations/notion/definitions/secrets.ts +++ b/integrations/notion/definitions/secrets.ts @@ -12,4 +12,7 @@ export const secrets = { WEBHOOK_VERIFICATION_SECRET: { description: 'The Notion-provided secret for verifying incoming webhooks.', }, + POSTHOG_KEY: { + description: 'The Posthog key of the Botpress Notion Integration.', + }, } as const satisfies sdk.IntegrationDefinitionProps['secrets'] diff --git a/integrations/notion/integration.definition.ts b/integrations/notion/integration.definition.ts index ede60edc364..a19e9858bab 100644 --- a/integrations/notion/integration.definition.ts +++ b/integrations/notion/integration.definition.ts @@ -2,11 +2,14 @@ import * as sdk from '@botpress/sdk' import filesReadonly from './bp_modules/files-readonly' import { actions, configuration, configurations, identifier, secrets, states, user } from './definitions' +export const INTEGRATION_NAME = 'notion' +export const INTEGRATION_VERSION = '2.2.1' + export default new sdk.IntegrationDefinition({ - name: 'notion', - description: 'Add pages and comments, manage databases, and engage in discussions — all within your chatbot.', + name: INTEGRATION_NAME, + version: INTEGRATION_VERSION, title: 'Notion', - version: '2.2.0', + description: 'Add pages and comments, manage databases, and engage in discussions — all within your chatbot.', icon: 'icon.svg', readme: 'hub.md', actions, diff --git a/integrations/notion/src/index.ts b/integrations/notion/src/index.ts index b1ec761cf24..462b43d6d82 100644 --- a/integrations/notion/src/index.ts +++ b/integrations/notion/src/index.ts @@ -1,19 +1,25 @@ -import { sentry as sentryHelpers } from '@botpress/sdk-addons' +import { posthogHelper } from '@botpress/common' +import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { actions } from './actions' import { register, unregister } from './setup' import { handler } from './webhook-events' import * as bp from '.botpress' -const integration = new bp.Integration({ - channels: {}, - register, - unregister, - actions, - handler, +@posthogHelper.wrapIntegration({ + integrationName: INTEGRATION_NAME, + integrationVersion: INTEGRATION_VERSION, + key: bp.secrets.POSTHOG_KEY, }) +class NotionIntegration extends bp.Integration { + public constructor() { + super({ + register, + unregister, + actions, + channels: {}, + handler, + }) + } +} -export default sentryHelpers.wrapIntegration(integration, { - dsn: bp.secrets.SENTRY_DSN, - environment: bp.secrets.SENTRY_ENVIRONMENT, - release: bp.secrets.SENTRY_RELEASE, -}) +export default new NotionIntegration() diff --git a/integrations/notion/src/setup.ts b/integrations/notion/src/setup.ts index 963840da0b5..157189f69f3 100644 --- a/integrations/notion/src/setup.ts +++ b/integrations/notion/src/setup.ts @@ -1,9 +1,18 @@ +import { RuntimeError } from '@botpress/sdk' import { NotionClient } from './notion-api' import * as bp from '.botpress' export const register: bp.IntegrationProps['register'] = async (props) => { const notionClient = await NotionClient.create(props) - await notionClient.testAuthentication() + await notionClient.testAuthentication().catch((thrown) => { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new RuntimeError(`Failed to test authentication: ${error.message}`) + }) } -export const unregister: bp.IntegrationProps['unregister'] = async () => {} +export const unregister: bp.IntegrationProps['unregister'] = async (props) => { + const { client } = props + await client.configureIntegration({ + identifier: null, + }) +} diff --git a/packages/common/src/posthog/helper.ts b/packages/common/src/posthog/helper.ts index 8a6c34bc4bd..8dc4297fcd3 100644 --- a/packages/common/src/posthog/helper.ts +++ b/packages/common/src/posthog/helper.ts @@ -1,5 +1,6 @@ import * as client from '@botpress/client' import * as sdk from '@botpress/sdk' + import { EventMessage, PostHog } from 'posthog-node' export const COMMON_SECRET_NAMES = { @@ -78,8 +79,12 @@ function wrapFunction(fn: Function, config: PostHogConfig) { return await fn(...args) } catch (thrown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) - const distinctId = client.isApiError(thrown) ? thrown.id : undefined + const additionalProps = { + configurationType: args[0]?.ctx?.configurationType, + integrationId: args[0]?.ctx?.integrationId, + } + await sendPosthogEvent( { distinctId: distinctId ?? 'no id', @@ -89,6 +94,7 @@ function wrapFunction(fn: Function, config: PostHogConfig) { integrationName: config.integrationName, integrationVersion: config.integrationVersion, errMsg, + ...additionalProps, }, }, config @@ -104,6 +110,11 @@ function wrapHandler(fn: Function, config: PostHogConfig) { return async (...args: any[]) => { const resp: void | Response = await fn(...args) if (resp instanceof Response && isServerErrorStatus(resp.status)) { + const additionalProps = { + configurationType: args[0]?.ctx?.configurationType, + integrationId: args[0]?.ctx?.integrationId, + } + if (!resp.body) { await sendPosthogEvent( { @@ -114,6 +125,7 @@ function wrapHandler(fn: Function, config: PostHogConfig) { integrationName: config.integrationName, integrationVersion: config.integrationVersion, errMsg: 'Empty Body', + ...additionalProps, }, }, config @@ -129,6 +141,7 @@ function wrapHandler(fn: Function, config: PostHogConfig) { integrationName: config.integrationName, integrationVersion: config.integrationVersion, errMsg: JSON.stringify(resp.body), + ...additionalProps, }, }, config diff --git a/plugins/hitl/plugin.definition.ts b/plugins/hitl/plugin.definition.ts index 806a0083ea8..27b95f94193 100644 --- a/plugins/hitl/plugin.definition.ts +++ b/plugins/hitl/plugin.definition.ts @@ -1,6 +1,7 @@ import * as sdk from '@botpress/sdk' import hitl from './bp_modules/hitl' +export const NULL_MESSAGE_CODE = 'NULL' export const DEFAULT_HITL_HANDOFF_MESSAGE = 'I have escalated this conversation to a human agent. They should be with you shortly.' export const DEFAULT_HUMAN_AGENT_ASSIGNED_MESSAGE = 'A human agent has joined the conversation.' @@ -14,59 +15,68 @@ export const DEFAULT_USER_HITL_COMMAND_MESSAGE = export const DEFAULT_AGENT_ASSIGNED_TIMEOUT_MESSAGE = 'No human agent is available at the moment. Please try again later. I will continue assisting you for the time being.' +const _nullCodeDescription = `Use "${NULL_MESSAGE_CODE}" to indicate that no message should be sent.` const PLUGIN_CONFIG_SCHEMA = sdk.z.object({ onHitlHandoffMessage: sdk.z .string() .title('Escalation Started Message') - .describe('The message to send to the user when transferring to a human agent') + .describe(`The message to send to the user when transferring to a human agent. ${_nullCodeDescription}`) .optional() .placeholder(DEFAULT_HITL_HANDOFF_MESSAGE), onHumanAgentAssignedMessage: sdk.z .string() .title('Human Agent Assigned Message') - .describe('The message to send to the user when a human agent is assigned') + .describe(`The message to send to the user when a human agent is assigned. ${_nullCodeDescription}`) .optional() .placeholder(DEFAULT_HUMAN_AGENT_ASSIGNED_MESSAGE), onHitlStoppedMessage: sdk.z .string() .title('Escalation Terminated Message') - .describe('The message to send to the user when the HITL session stops and control is transferred back to bot') + .describe( + `The message to send to the user when the HITL session stops and control is transferred back to bot. ${_nullCodeDescription}` + ) .optional() .placeholder(DEFAULT_HITL_STOPPED_MESSAGE), onUserHitlCancelledMessage: sdk.z .string() .title('Escalation Aborted Message') - .describe('The message to send to the human agent when the user abruptly ends the HITL session') + .describe( + `The message to send to the human agent when the user abruptly ends the HITL session. ${_nullCodeDescription}` + ) .optional() .placeholder(DEFAULT_USER_HITL_CANCELLED_MESSAGE), onIncompatibleMsgTypeMessage: sdk.z .string() .title('Incompatible Message Type Warning') .describe( - 'The warning to send to the human agent when they send a message that is not supported by the hitl session' + `The warning to send to the human agent when they send a message that is not supported by the hitl session. ${_nullCodeDescription}` ) .optional() .placeholder(DEFAULT_INCOMPATIBLE_MSGTYPE_MESSAGE), - userHitlCloseCommand: sdk.z - .string() - .title('Termination Command') - .describe( - 'Users may send this command to end the HITL session at any time. It is case-insensitive, so it works regardless of letter casing.' - ) - .optional() - .placeholder(DEFAULT_USER_HITL_CLOSE_COMMAND), onUserHitlCloseMessage: sdk.z .string() .title('Termination Command Message') - .describe('The message to send to the user when they end the HITL session using the termination command') + .describe( + `The message to send to the user when they end the HITL session using the termination command. ${_nullCodeDescription}` + ) .optional() .placeholder(DEFAULT_USER_HITL_COMMAND_MESSAGE), onAgentAssignedTimeoutMessage: sdk.z .string() .title('Agent Assigned Timeout Message') - .describe('The message to send to the user when no human agent is assigned within the timeout period') + .describe( + `The message to send to the user when no human agent is assigned within the timeout period. ${_nullCodeDescription}` + ) .optional() .placeholder(DEFAULT_AGENT_ASSIGNED_TIMEOUT_MESSAGE), + userHitlCloseCommand: sdk.z + .string() + .title('Termination Command') + .describe( + 'Users may send this command to end the HITL session at any time. It is case-insensitive, so it works regardless of letter casing.' + ) + .optional() + .placeholder(DEFAULT_USER_HITL_CLOSE_COMMAND), agentAssignedTimeoutSeconds: sdk.z .number() .title('Agent Assigned Timeout') @@ -92,7 +102,7 @@ const PLUGIN_CONFIG_SCHEMA = sdk.z.object({ export default new sdk.PluginDefinition({ name: 'hitl', - version: '1.1.1', + version: '1.2.0', title: 'Human In The Loop', description: 'Seamlessly transfer conversations to human agents', icon: 'icon.svg', diff --git a/plugins/hitl/src/actions/start-hitl.ts b/plugins/hitl/src/actions/start-hitl.ts index 56ca1d271da..3c9f3703298 100644 --- a/plugins/hitl/src/actions/start-hitl.ts +++ b/plugins/hitl/src/actions/start-hitl.ts @@ -76,13 +76,7 @@ export const startHitl: bp.PluginProps['actions']['startHitl'] = async (props) = const _sendHandoffMessage = ( upstreamCm: conv.ConversationManager, sessionConfig: bp.configuration.Configuration -): Promise => - upstreamCm.respond({ - type: 'text', - text: sessionConfig.onHitlHandoffMessage?.length - ? sessionConfig.onHitlHandoffMessage - : DEFAULT_HITL_HANDOFF_MESSAGE, - }) +): Promise => upstreamCm.maybeRespondText(sessionConfig.onHitlHandoffMessage, DEFAULT_HITL_HANDOFF_MESSAGE) const _buildMessageHistory = async ( upstreamConversation: types.ActionableConversation, diff --git a/plugins/hitl/src/actions/stop-hitl.ts b/plugins/hitl/src/actions/stop-hitl.ts index 476f3662822..9f7794dd22a 100644 --- a/plugins/hitl/src/actions/stop-hitl.ts +++ b/plugins/hitl/src/actions/stop-hitl.ts @@ -30,12 +30,7 @@ export const stopHitl: bp.PluginProps['actions']['stopHitl'] = async (props) => upstreamConversationId, }) - await downstreamCm.respond({ - type: 'text', - text: sessionConfig.onUserHitlCancelledMessage?.length - ? sessionConfig.onUserHitlCancelledMessage - : DEFAULT_USER_HITL_CANCELLED_MESSAGE, - }) + await downstreamCm.maybeRespondText(sessionConfig.onUserHitlCancelledMessage, DEFAULT_USER_HITL_CANCELLED_MESSAGE) await Promise.allSettled([ upstreamCm.setHitlInactive(conv.HITL_END_REASON.CLOSE_ACTION_CALLED), diff --git a/plugins/hitl/src/conv-manager.ts b/plugins/hitl/src/conv-manager.ts index 8be9f6bb1ff..50c90e60090 100644 --- a/plugins/hitl/src/conv-manager.ts +++ b/plugins/hitl/src/conv-manager.ts @@ -1,3 +1,4 @@ +import { NULL_MESSAGE_CODE } from 'plugin.definition' import * as types from './types' import * as bp from '.botpress' @@ -76,6 +77,14 @@ export class ConversationManager { }) } + public async maybeRespondText(message: string | undefined, defaultMsg: string): Promise { + if (message === NULL_MESSAGE_CODE) { + return + } + const text = message || defaultMsg + await this.respond({ type: 'text', text }) + } + public async respond(messagePayload: types.MessagePayload): 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 diff --git a/plugins/hitl/src/hooks/before-incoming-event/hitl-assigned.ts b/plugins/hitl/src/hooks/before-incoming-event/hitl-assigned.ts index e22bcdb6450..3cb01b18387 100644 --- a/plugins/hitl/src/hooks/before-incoming-event/hitl-assigned.ts +++ b/plugins/hitl/src/hooks/before-incoming-event/hitl-assigned.ts @@ -36,12 +36,7 @@ export const handleEvent: bp.HookHandlers['before_incoming_event']['hitl:hitlAss const humanAgentName = humanAgentUser?.name?.length ? humanAgentUser.name : 'A Human Agent' await Promise.all([ - upstreamCm.respond({ - type: 'text', - text: sessionConfig.onHumanAgentAssignedMessage?.length - ? sessionConfig.onHumanAgentAssignedMessage - : DEFAULT_HUMAN_AGENT_ASSIGNED_MESSAGE, - }), + upstreamCm.maybeRespondText(sessionConfig.onHumanAgentAssignedMessage, DEFAULT_HUMAN_AGENT_ASSIGNED_MESSAGE), downstreamCm.setHumanAgent(humanAgentUserId, humanAgentName), upstreamCm.setHumanAgent(humanAgentUserId, humanAgentName), tryLinkWebchatUser(props, { downstreamUser: humanAgentUser, upstreamConversation, forceLink: true }), diff --git a/plugins/hitl/src/hooks/before-incoming-event/hitl-stopped.ts b/plugins/hitl/src/hooks/before-incoming-event/hitl-stopped.ts index 0114fb01251..7c3e71a95dc 100644 --- a/plugins/hitl/src/hooks/before-incoming-event/hitl-stopped.ts +++ b/plugins/hitl/src/hooks/before-incoming-event/hitl-stopped.ts @@ -32,12 +32,7 @@ export const handleEvent: bp.HookHandlers['before_incoming_event']['hitl:hitlSto }) await Promise.allSettled([ - upstreamCm.respond({ - type: 'text', - text: sessionConfig.onHitlStoppedMessage?.length - ? sessionConfig.onHitlStoppedMessage - : DEFAULT_HITL_STOPPED_MESSAGE, - }), + upstreamCm.maybeRespondText(sessionConfig.onHitlStoppedMessage, DEFAULT_HITL_STOPPED_MESSAGE), downstreamCm.setHitlInactive(conv.HITL_END_REASON.AGENT_CLOSED_TICKET), upstreamCm.setHitlInactive(conv.HITL_END_REASON.AGENT_CLOSED_TICKET), ]) diff --git a/plugins/hitl/src/hooks/before-incoming-event/human-agent-assigned-timeout.ts b/plugins/hitl/src/hooks/before-incoming-event/human-agent-assigned-timeout.ts index 3faa6a2b740..7abb52acf12 100644 --- a/plugins/hitl/src/hooks/before-incoming-event/human-agent-assigned-timeout.ts +++ b/plugins/hitl/src/hooks/before-incoming-event/human-agent-assigned-timeout.ts @@ -79,13 +79,7 @@ const _handleTimeout = async ( downstreamCm: conv.ConversationManager, sessionConfig: bp.configuration.Configuration ) => { - await downstreamCm.respond({ - // TODO: We might want to add a custom message for the human agent. - type: 'text', - text: sessionConfig.onUserHitlCancelledMessage?.length - ? sessionConfig.onUserHitlCancelledMessage - : DEFAULT_USER_HITL_CANCELLED_MESSAGE, - }) + await downstreamCm.maybeRespondText(sessionConfig.onUserHitlCancelledMessage, DEFAULT_USER_HITL_CANCELLED_MESSAGE) await Promise.allSettled([ upstreamCm.setHitlInactive(conv.HITL_END_REASON.AGENT_ASSIGNMENT_TIMEOUT), @@ -100,10 +94,5 @@ const _handleTimeout = async ( // Call stopHitl in the hitl integration (zendesk, etc.): await props.actions.hitl.stopHitl({ conversationId: downstreamCm.conversationId }) - await upstreamCm.respond({ - type: 'text', - text: sessionConfig.onAgentAssignedTimeoutMessage?.length - ? sessionConfig.onAgentAssignedTimeoutMessage - : DEFAULT_AGENT_ASSIGNED_TIMEOUT_MESSAGE, - }) + await upstreamCm.maybeRespondText(sessionConfig.onAgentAssignedTimeoutMessage, DEFAULT_AGENT_ASSIGNED_TIMEOUT_MESSAGE) } diff --git a/plugins/hitl/src/hooks/before-incoming-message/all.ts b/plugins/hitl/src/hooks/before-incoming-message/all.ts index a5c06c73202..63fe3a31196 100644 --- a/plugins/hitl/src/hooks/before-incoming-message/all.ts +++ b/plugins/hitl/src/hooks/before-incoming-message/all.ts @@ -57,12 +57,10 @@ const _handleDownstreamMessage = async ( if (!messagePayload) { props.logger.with(props.data).error('Downstream conversation received a non-text message') - await downstreamCm.respond({ - type: 'text', - text: sessionConfig.onIncompatibleMsgTypeMessage?.length - ? sessionConfig.onIncompatibleMsgTypeMessage - : DEFAULT_INCOMPATIBLE_MSGTYPE_MESSAGE, - }) + await downstreamCm.maybeRespondText( + sessionConfig.onIncompatibleMsgTypeMessage, + DEFAULT_INCOMPATIBLE_MSGTYPE_MESSAGE + ) return consts.STOP_EVENT_HANDLING } @@ -191,12 +189,7 @@ const _handleHitlCloseCommand = async ( sessionConfig: bp.configuration.Configuration } ) => { - await downstreamCm.respond({ - type: 'text', - text: sessionConfig.onUserHitlCancelledMessage?.length - ? sessionConfig.onUserHitlCancelledMessage - : DEFAULT_USER_HITL_CANCELLED_MESSAGE, - }) + await downstreamCm.maybeRespondText(sessionConfig.onUserHitlCancelledMessage, DEFAULT_USER_HITL_CANCELLED_MESSAGE) await Promise.allSettled([ upstreamCm.setHitlInactive(conv.HITL_END_REASON.PATIENT_USED_TERMINATION_COMMAND), @@ -213,10 +206,5 @@ const _handleHitlCloseCommand = async ( // Call stopHitl in the hitl integration (zendesk, etc.): await props.actions.hitl.stopHitl({ conversationId: downstreamCm.conversationId }) - await upstreamCm.respond({ - type: 'text', - text: sessionConfig.onUserHitlCloseMessage?.length - ? sessionConfig.onUserHitlCloseMessage - : DEFAULT_USER_HITL_COMMAND_MESSAGE, - }) + await upstreamCm.maybeRespondText(sessionConfig.onUserHitlCloseMessage, DEFAULT_USER_HITL_COMMAND_MESSAGE) }