diff --git a/apps/api/src/app/agents/agents-webhook.controller.ts b/apps/api/src/app/agents/agents-webhook.controller.ts index 270aa1f2a8c..724a225f861 100644 --- a/apps/api/src/app/agents/agents-webhook.controller.ts +++ b/apps/api/src/app/agents/agents-webhook.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Get, HttpCode, HttpException, HttpStatus, @@ -56,6 +57,16 @@ export class AgentsWebhookController { ); } + @Get('/:agentId/webhook/:integrationIdentifier') + async handleWebhookVerification( + @Param('agentId') agentId: string, + @Param('integrationIdentifier') integrationIdentifier: string, + @Req() req: Request, + @Res() res: Response + ) { + return this.routeWebhook(agentId, integrationIdentifier, req, res); + } + @Post('/:agentId/webhook/:integrationIdentifier') @HttpCode(HttpStatus.OK) async handleInboundWebhook( @@ -64,12 +75,13 @@ export class AgentsWebhookController { @Req() req: Request, @Res() res: Response ) { + return this.routeWebhook(agentId, integrationIdentifier, req, res); + } + + private async routeWebhook(agentId: string, integrationIdentifier: string, req: Request, res: Response) { try { - console.log('handleInboundWebhook', agentId, integrationIdentifier); await this.chatSdkService.handleWebhook(agentId, integrationIdentifier, req, res); - console.log('handleInboundWebhook success'); } catch (err) { - console.log(err); if (err instanceof HttpException) { res.status(err.getStatus()).json(err.getResponse()); } else { diff --git a/apps/api/src/app/agents/dtos/agent-platform.enum.ts b/apps/api/src/app/agents/dtos/agent-platform.enum.ts index 674c27d7d4c..b1d01298bf7 100644 --- a/apps/api/src/app/agents/dtos/agent-platform.enum.ts +++ b/apps/api/src/app/agents/dtos/agent-platform.enum.ts @@ -3,3 +3,7 @@ export enum AgentPlatformEnum { WHATSAPP = 'whatsapp', TEAMS = 'teams', } + +export const PLATFORMS_WITHOUT_TYPING_INDICATOR = new Set([ + AgentPlatformEnum.WHATSAPP, +]); diff --git a/apps/api/src/app/agents/services/agent-conversation.service.ts b/apps/api/src/app/agents/services/agent-conversation.service.ts index 47ff8ae004b..3752408838b 100644 --- a/apps/api/src/app/agents/services/agent-conversation.service.ts +++ b/apps/api/src/app/agents/services/agent-conversation.service.ts @@ -32,6 +32,7 @@ export interface PersistInboundMessageParams { senderId: string; senderName?: string; content: string; + richContent?: Record; platformMessageId?: string; environmentId: string; organizationId: string; @@ -140,6 +141,7 @@ export class AgentConversationService { senderId: params.senderId, senderName: params.senderName, content: params.content, + richContent: params.richContent, platformMessageId: params.platformMessageId, environmentId: params.environmentId, organizationId: params.organizationId, diff --git a/apps/api/src/app/agents/services/agent-inbound-handler.service.ts b/apps/api/src/app/agents/services/agent-inbound-handler.service.ts index 64e36d87513..b3e8cebf67f 100644 --- a/apps/api/src/app/agents/services/agent-inbound-handler.service.ts +++ b/apps/api/src/app/agents/services/agent-inbound-handler.service.ts @@ -8,6 +8,7 @@ import { } from '@novu/dal'; import type { Message, Thread } from 'chat'; import { AgentEventEnum } from '../dtos/agent-event.enum'; +import { PLATFORMS_WITHOUT_TYPING_INDICATOR } from '../dtos/agent-platform.enum'; import { HandleAgentReplyCommand } from '../usecases/handle-agent-reply/handle-agent-reply.command'; import { HandleAgentReply } from '../usecases/handle-agent-reply/handle-agent-reply.usecase'; import { ResolvedAgentConfig } from './agent-config-resolver.service'; @@ -89,6 +90,18 @@ export class AgentInboundHandler { ? ConversationActivitySenderTypeEnum.SUBSCRIBER : ConversationActivitySenderTypeEnum.PLATFORM_USER; + const richContent = message.attachments?.length + ? { + attachments: message.attachments.map((a) => ({ + type: a.type, + url: a.url, + name: a.name, + mimeType: a.mimeType, + size: a.size, + })), + } + : undefined; + await this.conversationService.persistInboundMessage({ conversationId: conversation._id, platform: config.platform, @@ -98,6 +111,7 @@ export class AgentInboundHandler { senderId: participantId, senderName: message.author.fullName, content: message.text, + richContent, platformMessageId: message.id, environmentId: config.environmentId, organizationId: config.organizationId, @@ -121,7 +135,7 @@ export class AgentInboundHandler { }); } - if (config.thinkingIndicatorEnabled) { + if (config.thinkingIndicatorEnabled && !PLATFORMS_WITHOUT_TYPING_INDICATOR.has(config.platform)) { await thread.startTyping('Thinking...'); } diff --git a/apps/api/src/app/agents/services/bridge-executor.service.ts b/apps/api/src/app/agents/services/bridge-executor.service.ts index d0ccdc7cfe1..39eeb7e9742 100644 --- a/apps/api/src/app/agents/services/bridge-executor.service.ts +++ b/apps/api/src/app/agents/services/bridge-executor.service.ts @@ -51,11 +51,20 @@ interface BridgeMessageAuthor { isBot: boolean | 'unknown'; } +interface BridgeAttachment { + type: string; + url?: string; + name?: string; + mimeType?: string; + size?: number; +} + interface BridgeMessage { text: string; platformMessageId: string; author: BridgeMessageAuthor; timestamp: string; + attachments?: BridgeAttachment[]; } export interface BridgeAction { @@ -87,6 +96,7 @@ interface BridgeHistoryEntry { role: ConversationActivitySenderTypeEnum; type: ConversationActivityTypeEnum; content: string; + richContent?: Record; senderName?: string; signalData?: { type: string; payload?: Record }; createdAt: string; @@ -282,7 +292,7 @@ export class BridgeExecutorService { } private mapMessage(message: Message): BridgeMessage { - return { + const mapped: BridgeMessage = { text: message.text, platformMessageId: message.id, author: { @@ -293,6 +303,18 @@ export class BridgeExecutorService { }, timestamp: message.metadata?.dateSent?.toISOString() ?? new Date().toISOString(), }; + + if (message.attachments?.length) { + mapped.attachments = message.attachments.map((a) => ({ + type: a.type, + url: a.url, + name: a.name, + mimeType: a.mimeType, + size: a.size, + })); + } + + return mapped; } private mapConversation(conversation: ConversationEntity): BridgeConversation { @@ -337,6 +359,7 @@ export class BridgeExecutorService { role: activity.senderType, type: activity.type, content: activity.content, + richContent: activity.richContent || undefined, senderName: activity.senderName || undefined, signalData: activity.signalData || undefined, createdAt: activity.createdAt, diff --git a/apps/api/src/app/agents/services/chat-sdk.service.ts b/apps/api/src/app/agents/services/chat-sdk.service.ts index f713dadf90b..70b447df520 100644 --- a/apps/api/src/app/agents/services/chat-sdk.service.ts +++ b/apps/api/src/app/agents/services/chat-sdk.service.ts @@ -20,9 +20,9 @@ import { AgentInboundHandler } from './agent-inbound-handler.service'; * credentials.secretKey → appPassword * credentials.tenantId → appTenantId * - * WhatsApp: credentials.token → accessToken + * WhatsApp: credentials.apiToken → accessToken * credentials.secretKey → appSecret - * credentials.apiToken → verifyToken + * credentials.token → verifyToken * credentials.phoneNumberIdentification → phoneNumberId */ @@ -224,35 +224,49 @@ export class ChatSdkService implements OnModuleDestroy { switch (platform) { case AgentPlatformEnum.SLACK: { + if (!connectionAccessToken || !credentials.signingSecret) { + throw new BadRequestException('Slack agent integration requires botToken and signingSecret credentials'); + } + const { createSlackAdapter } = await esmImport('@chat-adapter/slack'); return { slack: createSlackAdapter({ - botToken: connectionAccessToken!, - signingSecret: credentials.signingSecret!, + botToken: connectionAccessToken, + signingSecret: credentials.signingSecret, }), }; } case AgentPlatformEnum.TEAMS: { + if (!credentials.clientId || !credentials.secretKey || !credentials.tenantId) { + throw new BadRequestException('Teams agent integration requires appId, appPassword, and appTenantId credentials'); + } + const { createTeamsAdapter } = await esmImport('@chat-adapter/teams'); return { teams: createTeamsAdapter({ - appId: credentials.clientId!, - appPassword: credentials.secretKey!, - appTenantId: credentials.tenantId!, + appId: credentials.clientId, + appPassword: credentials.secretKey, + appTenantId: credentials.tenantId, }), }; } case AgentPlatformEnum.WHATSAPP: { + if (!credentials.apiToken || !credentials.secretKey || !credentials.token || !credentials.phoneNumberIdentification) { + throw new BadRequestException( + 'WhatsApp agent integration requires accessToken, appSecret, verifyToken, and phoneNumberId credentials' + ); + } + const { createWhatsAppAdapter } = await esmImport('@chat-adapter/whatsapp'); return { whatsapp: createWhatsAppAdapter({ - accessToken: credentials.token!, - appSecret: credentials.secretKey!, - verifyToken: credentials.apiToken!, - phoneNumberId: credentials.phoneNumberIdentification!, + accessToken: credentials.apiToken, + appSecret: credentials.secretKey, + verifyToken: credentials.token, + phoneNumberId: credentials.phoneNumberIdentification, }), }; } diff --git a/libs/dal/src/repositories/conversation-activity/conversation-activity.repository.ts b/libs/dal/src/repositories/conversation-activity/conversation-activity.repository.ts index 9698e7000dc..d28144e5d32 100644 --- a/libs/dal/src/repositories/conversation-activity/conversation-activity.repository.ts +++ b/libs/dal/src/repositories/conversation-activity/conversation-activity.repository.ts @@ -54,6 +54,7 @@ export class ConversationActivityRepository extends BaseRepositoryV2< senderType: ConversationActivitySenderTypeEnum; senderId: string; content: string; + richContent?: Record; platformMessageId?: string; senderName?: string; environmentId: string; @@ -69,6 +70,7 @@ export class ConversationActivityRepository extends BaseRepositoryV2< senderType: params.senderType, senderId: params.senderId, content: params.content, + richContent: params.richContent, platformMessageId: params.platformMessageId, senderName: params.senderName, _environmentId: params.environmentId, diff --git a/packages/framework/src/resources/agent/agent.types.ts b/packages/framework/src/resources/agent/agent.types.ts index 79937a604b3..d746e10fbc9 100644 --- a/packages/framework/src/resources/agent/agent.types.ts +++ b/packages/framework/src/resources/agent/agent.types.ts @@ -18,11 +18,20 @@ export interface AgentMessageAuthor { isBot: boolean | 'unknown'; } +export interface AgentAttachment { + type: string; + url?: string; + name?: string; + mimeType?: string; + size?: number; +} + export interface AgentMessage { text: string; platformMessageId: string; author: AgentMessageAuthor; timestamp: string; + attachments?: AgentAttachment[]; } export interface AgentConversation { @@ -49,6 +58,7 @@ export interface AgentHistoryEntry { role: string; type: string; content: string; + richContent?: Record; senderName?: string; signalData?: { type: string; payload?: Record }; createdAt: string; diff --git a/packages/framework/src/resources/agent/index.ts b/packages/framework/src/resources/agent/index.ts index f05cc4958c6..efad3dd9439 100644 --- a/packages/framework/src/resources/agent/index.ts +++ b/packages/framework/src/resources/agent/index.ts @@ -16,6 +16,7 @@ export { agent } from './agent.resource'; export type { Agent, AgentAction, + AgentAttachment, AgentBridgeRequest, AgentContext, AgentConversation, diff --git a/packages/shared/src/consts/providers/conversational-providers.ts b/packages/shared/src/consts/providers/conversational-providers.ts index 80f14456513..c89ab4ab698 100644 --- a/packages/shared/src/consts/providers/conversational-providers.ts +++ b/packages/shared/src/consts/providers/conversational-providers.ts @@ -9,7 +9,7 @@ export type ConversationalProvider = { export const CONVERSATIONAL_PROVIDERS: ConversationalProvider[] = [ { providerId: ChatProviderIdEnum.Slack, displayName: 'Slack' }, { providerId: ChatProviderIdEnum.MsTeams, displayName: 'MS Teams', comingSoon: true }, - { providerId: ChatProviderIdEnum.WhatsAppBusiness, displayName: 'WhatsApp Business', comingSoon: true }, + { providerId: ChatProviderIdEnum.WhatsAppBusiness, displayName: 'WhatsApp Business' }, { providerId: 'telegram', displayName: 'Telegram', comingSoon: true }, { providerId: 'google-chat', displayName: 'Google Chat', comingSoon: true }, { providerId: 'linear', displayName: 'Linear', comingSoon: true }, diff --git a/packages/shared/src/consts/providers/credentials/provider-credentials.ts b/packages/shared/src/consts/providers/credentials/provider-credentials.ts index b1713f5be42..64cef42c759 100644 --- a/packages/shared/src/consts/providers/credentials/provider-credentials.ts +++ b/packages/shared/src/consts/providers/credentials/provider-credentials.ts @@ -1290,6 +1290,20 @@ export const whatsAppBusinessConfig: IConfigCredential[] = [ type: 'string', required: true, }, + { + key: CredentialsKeyEnum.SecretKey, + displayName: 'App Secret', + description: 'Found under App Settings > Basic in your Meta app dashboard — used to verify inbound webhook signatures', + type: 'string', + required: false, + }, + { + key: CredentialsKeyEnum.Token, + displayName: 'Verify Token', + description: 'A secret string you define — must match the Verify Token entered in your Meta webhook configuration', + type: 'string', + required: false, + }, ]; export const mobishastraConfig: IConfigCredential[] = [