From fdca83a356631547a202cbdb5face774bca3db18 Mon Sep 17 00:00:00 2001 From: Mathieu Faucher <99497774+Math-Fauch@users.noreply.github.com> Date: Wed, 6 May 2026 10:29:32 -0400 Subject: [PATCH 1/3] fix(integrations/anthropic): deprecate temperature (#15163) Co-authored-by: FelixGCliche <48337098+FelixGCliche@users.noreply.github.com> --- .../anthropic/integration.definition.ts | 2 +- .../anthropic/src/actions/generate-content.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/integrations/anthropic/integration.definition.ts b/integrations/anthropic/integration.definition.ts index 6857b36cd00..d63c155ca53 100644 --- a/integrations/anthropic/integration.definition.ts +++ b/integrations/anthropic/integration.definition.ts @@ -6,7 +6,7 @@ export default new IntegrationDefinition({ name: 'anthropic', title: 'Anthropic', description: 'Access a curated list of Claude models to set as your chosen LLM.', - version: '18.0.0', + version: '18.0.1', readme: 'hub.md', icon: 'icon.svg', entities: { diff --git a/integrations/anthropic/src/actions/generate-content.ts b/integrations/anthropic/src/actions/generate-content.ts index 7261c795a34..5a53616b632 100644 --- a/integrations/anthropic/src/actions/generate-content.ts +++ b/integrations/anthropic/src/actions/generate-content.ts @@ -99,18 +99,23 @@ export async function generateContent( messages, } - if ( + // TODO: Remove this check once all 3.x models are removed, + // see https://platform.claude.com/docs/en/about-claude/models/migration-guide#breaking-changes + if (modelId === 'claude-opus-4-7') { + // This is a stricter check for Opuse 4.7 as it does not support sampling parameters + // see https://platform.claude.com/docs/en/about-claude/models/migration-guide#additional-breaking-changes + request.top_k = undefined + request.top_p = undefined + request.temperature = undefined + } else if ( (modelId === 'claude-sonnet-4-5-20250929' || modelId === 'claude-haiku-4-5-20251001' || modelId === 'claude-sonnet-4-6' || - modelId === 'claude-opus-4-6' || - modelId === 'claude-opus-4-7') && + modelId === 'claude-opus-4-6') && request.temperature !== undefined && request.top_p !== undefined ) { - // TODO: Remove this check once all 3.x models are removed, - // see https://platform.claude.com/docs/en/about-claude/models/migration-guide#breaking-changes - request.top_p = undefined + request.temperature = undefined } const thinkingBudgetTokens = ThinkingModeBudgetTokens[input.reasoningEffort ?? 'none'] // Default to not use reasoning as Claude models use optional reasoning From ff398ec810a494fe1e58c4e65be43063d03508f1 Mon Sep 17 00:00:00 2001 From: David Vitor Antonio Date: Wed, 6 May 2026 11:50:14 -0300 Subject: [PATCH 2/3] feat(telegram): add fallback for unsupported file types (#15152) --- .../telegram/integration.definition.ts | 2 +- .../telegram/src/misc/message-handlers.ts | 122 ++++++++++++------ 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/integrations/telegram/integration.definition.ts b/integrations/telegram/integration.definition.ts index 0a1e7249b5f..c8ce3de4fe1 100644 --- a/integrations/telegram/integration.definition.ts +++ b/integrations/telegram/integration.definition.ts @@ -5,7 +5,7 @@ import { telegramMessageChannels } from './definitions/channels' export default new IntegrationDefinition({ name: 'telegram', - version: '1.0.6', + version: '1.0.7', title: 'Telegram', description: 'Engage with your audience in real-time.', icon: 'icon.svg', diff --git a/integrations/telegram/src/misc/message-handlers.ts b/integrations/telegram/src/misc/message-handlers.ts index fbd98461193..e49d1f5debf 100644 --- a/integrations/telegram/src/misc/message-handlers.ts +++ b/integrations/telegram/src/misc/message-handlers.ts @@ -1,6 +1,7 @@ import { RuntimeError } from '@botpress/client' -import { Markup, Telegraf } from 'telegraf' +import { Markup, Telegraf, Telegram } from 'telegraf' import { markdownHtmlToTelegramPayloads, stdMarkdownToTelegramHtml } from './markdown-to-telegram-html' +import { TelegramMessage } from './types' import { ackMessage, getChat, mapToRuntimeErrorAndThrow, sendCard } from './utils' import * as bp from '.botpress' @@ -20,6 +21,52 @@ const sendHtmlTextMessage = async ( await ackMessage(message, ack) } +type SendMediaMethod = ( + chatId: number | string, + media: string, + extra?: { caption?: string } +) => Promise + +const sendContentOrFallback = async

>({ + props, + url, + send, + fallback, +}: { + props: P + url: string + send: (telegram: Telegram) => SendMediaMethod + fallback?: () => Promise +}) => { + const { ctx, conversation, ack, logger, payload } = props + const client = new Telegraf(ctx.configuration.botToken) + const chat = getChat(conversation) + const sendFn = send(client.telegram) + const opts = 'caption' in payload ? { caption: payload.caption } : undefined + logger.forBot().debug(`calling ${sendFn.name} to Telegram chat ${chat}: ${url}`) + let message: TelegramMessage + try { + message = await sendFn + .call(client.telegram, chat, url, opts) + .catch(mapToRuntimeErrorAndThrow(`Failed to ${sendFn.name}`)) + } catch (err) { + if (fallback) { + await fallback() + return + } + logger + .forBot() + .warn( + `Telegram could not send the media using ${sendFn.name}, sending it as a plain text link instead: ${String(err)}` + ) + const text = opts?.caption ? `${opts.caption}\n${url}` : url + message = await client.telegram + .sendMessage(chat, text) + .catch(mapToRuntimeErrorAndThrow('Fail to send media link fallback')) + } + await ackMessage(message, ack) +} + export const handleTextMessage = async (props: MessageHandlerProps<'text'>) => { const { payload, ctx, conversation, ack, logger } = props const { text } = payload @@ -44,54 +91,43 @@ export const handleTextMessage = async (props: MessageHandlerProps<'text'>) => { } } -export const handleImageMessage = async ({ payload, ctx, conversation, ack, logger }: MessageHandlerProps<'image'>) => { - const client = new Telegraf(ctx.configuration.botToken) - const chat = getChat(conversation) - logger.forBot().debug(`Sending image message to Telegram chat ${chat}`, payload.imageUrl) - const message = await client.telegram - .sendPhoto(chat, payload.imageUrl, { - caption: payload.caption, - }) - .catch(mapToRuntimeErrorAndThrow('Fail to send photo')) - await ackMessage(message, ack) +export const handleImageMessage = async (props: MessageHandlerProps<'image'>) => { + await sendContentOrFallback({ + props, + url: props.payload.imageUrl, + send: (t) => t.sendPhoto, + }) } -export const handleAudioMessage = async ({ payload, ctx, conversation, ack, logger }: MessageHandlerProps<'audio'>) => { - const client = new Telegraf(ctx.configuration.botToken) - const chat = getChat(conversation) - logger.forBot().debug(`Sending audio voice to Telegram chat ${chat}:`, payload.audioUrl) - try { - const message = await client.telegram - .sendVoice(chat, payload.audioUrl, { caption: payload.caption }) - .catch(mapToRuntimeErrorAndThrow('Fail to send voice')) - await ackMessage(message, ack) - } catch { - // If the audio file is too large to be voice, Telegram should send it as an audio file, but if for some reason it doesn't, we can send it as an audio file - const message = await client.telegram - .sendAudio(chat, payload.audioUrl, { caption: payload.caption }) - .catch(mapToRuntimeErrorAndThrow('Fail to send audio')) - await ackMessage(message, ack) - } +export const handleAudioMessage = async (props: MessageHandlerProps<'audio'>) => { + // If voice fails, retry as audio; if that also fails, fall back to a plain text link. + await sendContentOrFallback({ + props, + url: props.payload.audioUrl, + send: (t) => t.sendVoice, + fallback: () => + sendContentOrFallback({ + props, + url: props.payload.audioUrl, + send: (t) => t.sendAudio, + }), + }) } -export const handleVideoMessage = async ({ payload, ctx, conversation, ack, logger }: MessageHandlerProps<'video'>) => { - const client = new Telegraf(ctx.configuration.botToken) - const chat = getChat(conversation) - logger.forBot().debug(`Sending video message to Telegram chat ${chat}:`, payload.videoUrl) - const message = await client.telegram - .sendVideo(chat, payload.videoUrl) - .catch(mapToRuntimeErrorAndThrow('Fail to send video')) - await ackMessage(message, ack) +export const handleVideoMessage = async (props: MessageHandlerProps<'video'>) => { + await sendContentOrFallback({ + props, + url: props.payload.videoUrl, + send: (t) => t.sendVideo, + }) } -export const handleFileMessage = async ({ payload, ctx, conversation, ack, logger }: MessageHandlerProps<'file'>) => { - const client = new Telegraf(ctx.configuration.botToken) - const chat = getChat(conversation) - logger.forBot().debug(`Sending file message to Telegram chat ${chat}:`, payload.fileUrl) - const message = await client.telegram - .sendDocument(chat, payload.fileUrl) - .catch(mapToRuntimeErrorAndThrow('Fail to send document')) - await ackMessage(message, ack) +export const handleFileMessage = async (props: MessageHandlerProps<'file'>) => { + await sendContentOrFallback({ + props, + url: props.payload.fileUrl, + send: (t) => t.sendDocument, + }) } export const handleLocationMessage = async ({ From e4a5a77cf01f9b9519c7eee48bd8c6676ae3feb2 Mon Sep 17 00:00:00 2001 From: David Vitor Antonio Date: Wed, 6 May 2026 12:23:41 -0300 Subject: [PATCH 3/3] feat(whatsapp): add text url fallback for media and status tag (#15158) --- .../whatsapp/integration.definition.ts | 6 +- .../whatsapp/src/webhook/handlers/status.ts | 192 +++++++++++------- 2 files changed, 118 insertions(+), 80 deletions(-) diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index 876e0a854ac..a1a97221130 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -157,7 +157,7 @@ const defaultBotPhoneNumberId = { } export const INTEGRATION_NAME = 'whatsapp' -export const INTEGRATION_VERSION = '4.13.1' +export const INTEGRATION_VERSION = '4.14.0' export default new IntegrationDefinition({ name: INTEGRATION_NAME, version: INTEGRATION_VERSION, @@ -342,6 +342,10 @@ export default new IntegrationDefinition({ title: 'Echo Creation Type', description: 'For echoed messages: the creation type reported by WhatsApp (e.g. "created_by_1p_bot")', }, + status: { + title: 'Delivery Status', + description: 'Latest WhatsApp delivery status reported via webhook (SENT, DELIVERED, READ, FAILED).', + }, }, }, conversation: { diff --git a/integrations/whatsapp/src/webhook/handlers/status.ts b/integrations/whatsapp/src/webhook/handlers/status.ts index 6144791c7e0..11cfc2decd3 100644 --- a/integrations/whatsapp/src/webhook/handlers/status.ts +++ b/integrations/whatsapp/src/webhook/handlers/status.ts @@ -1,99 +1,133 @@ +import { getAuthenticatedWhatsappClient } from 'src/auth' import { getMessageFromWhatsappMessageId } from 'src/misc/util' +import { Text } from 'whatsapp-api-js/messages' import { WhatsAppStatusValue } from '../../misc/types' import * as bp from '.botpress' -export const statusHandler = async (value: WhatsAppStatusValue, props: bp.HandlerProps) => { - const { client, logger } = props - - if (value.status === 'sent') { - const message = await getMessageFromWhatsappMessageId(value.id, client) - if (!message) { - logger - .forBot() - .debug( - `The WhatsApp message was sent, but there is no corresponding message in Botpress. Botpress cannot create a messageSent event for WhatsApp message ID: ${value.id}` - ) - return - } - - await client.createEvent({ - type: 'messageSent', - conversationId: message.conversationId, - messageId: message.id, - payload: {}, - }) - } +// Meta error codes for which a plain-text URL fallback is useful. +// 131053 = Media upload error (Meta rejected the media format/codec). +// 131052 = Media download error (Meta couldn't fetch the URL). +const MEDIA_FAILURE_CODES = new Set([131052, 131053]) - if (value.status === 'delivered') { - const message = await getMessageFromWhatsappMessageId(value.id, client) - if (!message) { - logger - .forBot() - .debug( - `The WhatsApp message was delivered, but there is no corresponding message in Botpress. Botpress cannot create a messageDelivered event for WhatsApp message ID: ${value.id}` - ) - return - } - - await client.createEvent({ - type: 'messageDelivered', - conversationId: message.conversationId, - messageId: message.id, - payload: {}, - }) - } +// WhatsApp status progression: SENT → DELIVERED → READ. FAILED is terminal. +const STATUS_RANK: Record = { + SENT: 1, + DELIVERED: 2, + READ: 3, + FAILED: 4, +} - if (value.status === 'read') { - const message = await getMessageFromWhatsappMessageId(value.id, client) - if (!message) { - logger - .forBot() - .debug( - `The WhatsApp message was read, but there is no corresponding message in Botpress. Botpress cannot create a messageRead event for WhatsApp message ID: ${value.id}` - ) - return - } +const _isDuplicateOrStale = (incoming: WhatsAppStatusValue['status'], existing: string | undefined): boolean => { + const incomingRank = STATUS_RANK[incoming.toUpperCase()] ?? 0 + const existingRank = STATUS_RANK[(existing ?? '').toUpperCase()] ?? 0 + return existingRank >= incomingRank +} - await client.createEvent({ - type: 'messageRead', - conversationId: message.conversationId, - messageId: message.id, - payload: {}, - }) +const _getMediaUrlFromPayload = ( + message: { type: string; payload: { [k: string]: any } } | undefined +): string | undefined => { + if (!message) return undefined + switch (message.type) { + case 'audio': + return message.payload.audioUrl + case 'video': + return message.payload.videoUrl + case 'image': + return message.payload.imageUrl + case 'file': + return message.payload.fileUrl + default: + return undefined } +} - if (value.status === 'failed') { - const errorDetails = - value.errors - ?.map( - (err) => - `${err.title} (${err.code}): ${err.message}${err.error_data?.details ? ` - ${err.error_data.details}` : ''}` - ) - .join('; ') || 'Unknown error' +export const statusHandler = async (value: WhatsAppStatusValue, props: bp.HandlerProps) => { + const { client, ctx, logger } = props + const message = await getMessageFromWhatsappMessageId(value.id, client) + if (!message) { logger .forBot() - .error( - `WhatsApp message delivery failed. Message ID: ${value.id}, Recipient: ${value.recipient_id}, Errors: ${errorDetails}` + .debug( + `Received WhatsApp "${value.status}" webhook, but there is no corresponding message in Botpress for WhatsApp message ID: ${value.id}` ) + return + } + + const isStale = _isDuplicateOrStale(value.status, message.tags.status) + if (!isStale) { + await client.updateMessage({ id: message.id, tags: { status: value.status.toUpperCase() } }) + } + + switch (value.status) { + case 'sent': + await client.createEvent({ + type: 'messageSent', + conversationId: message.conversationId, + messageId: message.id, + payload: {}, + }) + break + case 'delivered': + await client.createEvent({ + type: 'messageDelivered', + conversationId: message.conversationId, + messageId: message.id, + payload: {}, + }) + break + case 'read': + await client.createEvent({ + type: 'messageRead', + conversationId: message.conversationId, + messageId: message.id, + payload: {}, + }) + break + case 'failed': { + const errorDetails = + value.errors + ?.map( + (err) => + `${err.title} (${err.code}): ${err.message}${err.error_data?.details ? ` - ${err.error_data.details}` : ''}` + ) + .join('; ') || 'Unknown error' - const message = await getMessageFromWhatsappMessageId(value.id, client) - if (!message) { logger .forBot() - .debug( - `The WhatsApp message delivery failed, but there is no corresponding message in Botpress. Botpress cannot create a messageFailed event for WhatsApp message ID: ${value.id}` + .error( + `WhatsApp message delivery failed. Message ID: ${value.id}, Recipient: ${value.recipient_id}, Errors: ${errorDetails}` ) - return - } - await client.createEvent({ - type: 'messageFailed', - conversationId: message.conversationId, - messageId: message.id, - payload: { - error: errorDetails, - }, - }) + const mediaErrorCode = value.errors?.find((e) => MEDIA_FAILURE_CODES.has(e.code))?.code + const mediaUrl = _getMediaUrlFromPayload(message) + if (!isStale && mediaErrorCode && mediaUrl) { + try { + const { conversation } = await client.getConversation({ id: message.conversationId }) + const { botPhoneNumberId, userPhone } = conversation.tags + if (botPhoneNumberId && userPhone) { + logger + .forBot() + .warn( + `WhatsApp rejected the ${message.type} (code ${mediaErrorCode}); sending the URL as a plain text fallback to ${userPhone}.` + ) + const whatsapp = await getAuthenticatedWhatsappClient(client, ctx) + await whatsapp.sendMessage(botPhoneNumberId, userPhone, new Text(mediaUrl)) + } + } catch (err) { + logger.forBot().error('Failed to send the plain text URL fallback for the rejected media:', err) + } + } + + await client.createEvent({ + type: 'messageFailed', + conversationId: message.conversationId, + messageId: message.id, + payload: { + error: errorDetails, + }, + }) + break + } } }