diff --git a/apps/api/src/app/agents/dtos/agent-event.enum.ts b/apps/api/src/app/agents/dtos/agent-event.enum.ts index f00cc60e737..84fc00ee42e 100644 --- a/apps/api/src/app/agents/dtos/agent-event.enum.ts +++ b/apps/api/src/app/agents/dtos/agent-event.enum.ts @@ -1,6 +1 @@ -export enum AgentEventEnum { - ON_MESSAGE = 'onMessage', - ON_ACTION = 'onAction', - ON_RESOLVE = 'onResolve', - ON_REACTION = 'onReaction', -} +export { AgentEventEnum } from '@novu/framework'; diff --git a/apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts b/apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts index 11e085269f5..a2b8e6bd499 100644 --- a/apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts +++ b/apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts @@ -13,14 +13,31 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; +import type { FileRef } from '@novu/framework'; + +export type { FileRef } from '@novu/framework'; const SIGNAL_TYPES = ['metadata', 'trigger'] as const; -export interface FileRef { - filename: string; - mimeType?: string; - data?: string; - url?: string; +@ValidatorConstraint({ name: 'isValidSignal', async: false }) +export class IsValidSignal implements ValidatorConstraintInterface { + validate(signal: SignalDto): boolean { + if (!signal?.type) return false; + + if (signal.type === 'metadata') { + return typeof signal.key === 'string' && signal.key.length > 0 && signal.value !== undefined; + } + + if (signal.type === 'trigger') { + return typeof signal.workflowId === 'string' && signal.workflowId.length > 0; + } + + return false; + } + + defaultMessage(): string { + return 'metadata signals require key + value; trigger signals require workflowId.'; + } } @ValidatorConstraint({ name: 'isValidReplyContent', async: false }) @@ -146,6 +163,7 @@ export class AgentReplyPayloadDto { @IsOptional() @IsArray() @ValidateNested({ each: true }) + @Validate(IsValidSignal, { each: true }) @Type(() => SignalDto) signals?: SignalDto[]; } 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 b3e8cebf67f..561aed44963 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 @@ -14,12 +14,8 @@ import { HandleAgentReply } from '../usecases/handle-agent-reply/handle-agent-re import { ResolvedAgentConfig } from './agent-config-resolver.service'; import { AgentConversationService } from './agent-conversation.service'; import { AgentSubscriberResolver } from './agent-subscriber-resolver.service'; -import { - type BridgeAction, - BridgeExecutorService, - type BridgeReaction, - NoBridgeUrlError, -} from './bridge-executor.service'; +import type { AgentAction } from '@novu/framework'; +import { BridgeExecutorService, type BridgeReaction, NoBridgeUrlError } from './bridge-executor.service'; const ONBOARDING_NO_BRIDGE_REPLY_MARKDOWN = `*You're connected to Novu* @@ -263,7 +259,7 @@ export class AgentInboundHandler { agentId: string, config: ResolvedAgentConfig, thread: Thread, - action: BridgeAction, + action: AgentAction, userId: string ): Promise { const subscriberId = await this.subscriberResolver 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 39eeb7e9742..afdc317441f 100644 --- a/apps/api/src/app/agents/services/bridge-executor.service.ts +++ b/apps/api/src/app/agents/services/bridge-executor.service.ts @@ -5,26 +5,24 @@ import { GetDecryptedSecretKeyCommand, PinoLogger, } from '@novu/application-generic'; -import { - ConversationActivityEntity, - ConversationActivitySenderTypeEnum, - ConversationActivityTypeEnum, - ConversationEntity, - SubscriberEntity, -} from '@novu/dal'; +import { ConversationActivityEntity, ConversationEntity, SubscriberEntity } from '@novu/dal'; +import type { + AgentAction, + AgentBridgeRequest, + AgentConversation, + AgentHistoryEntry, + AgentMessage, + AgentPlatformContext, + AgentReaction, + AgentSubscriber, +} from '@novu/framework'; +import { AgentEventEnum } from '@novu/framework'; import type { Message } from 'chat'; -import { AgentEventEnum } from '../dtos/agent-event.enum'; import { ResolvedAgentConfig } from './agent-config-resolver.service'; const MAX_RETRIES = 2; const RETRY_BASE_DELAY_MS = 500; -export interface BridgePlatformContext { - threadId: string; - channelId: string; - isDM: boolean; -} - export interface BridgeReaction { emoji: string; added: boolean; @@ -39,95 +37,11 @@ export interface BridgeExecutorParams { subscriber: SubscriberEntity | null; history: ConversationActivityEntity[]; message: Message | null; - platformContext: BridgePlatformContext; - action?: BridgeAction; + platformContext: AgentPlatformContext; + action?: AgentAction; reaction?: BridgeReaction; } -interface BridgeMessageAuthor { - userId: string; - fullName: string; - userName: string; - 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 { - actionId: string; - value?: string; -} - -interface BridgeConversation { - identifier: string; - status: string; - metadata: Record; - messageCount: number; - createdAt: string; - lastActivityAt: string; -} - -interface BridgeSubscriber { - subscriberId: string; - firstName?: string; - lastName?: string; - email?: string; - phone?: string; - avatar?: string; - locale?: string; - data?: Record; -} - -interface BridgeHistoryEntry { - role: ConversationActivitySenderTypeEnum; - type: ConversationActivityTypeEnum; - content: string; - richContent?: Record; - senderName?: string; - signalData?: { type: string; payload?: Record }; - createdAt: string; -} - -interface BridgeReactionPayload { - messageId: string; - emoji: { name: string }; - added: boolean; - message: BridgeMessage | null; -} - -export interface AgentBridgeRequest { - version: 1; - timestamp: string; - deliveryId: string; - event: AgentEventEnum; - agentId: string; - replyUrl: string; - conversationId: string; - integrationIdentifier: string; - message: BridgeMessage | null; - conversation: BridgeConversation; - subscriber: BridgeSubscriber | null; - history: BridgeHistoryEntry[]; - platform: string; - platformContext: BridgePlatformContext; - action: BridgeAction | null; - reaction: BridgeReactionPayload | null; -} - export class NoBridgeUrlError extends Error { constructor(agentIdentifier: string) { super(`No bridge URL configured for agent ${agentIdentifier}`); @@ -269,7 +183,7 @@ export class BridgeExecutorService { deliveryId = `${conversation._id}:${event}`; } - const payload: AgentBridgeRequest = { + return { version: 1, timestamp, deliveryId, @@ -287,12 +201,10 @@ export class BridgeExecutorService { action: action ?? null, reaction: reaction ? this.mapReaction(reaction) : null, }; - - return payload; } - private mapMessage(message: Message): BridgeMessage { - const mapped: BridgeMessage = { + private mapMessage(message: Message): AgentMessage { + const mapped: AgentMessage = { text: message.text, platformMessageId: message.id, author: { @@ -317,7 +229,7 @@ export class BridgeExecutorService { return mapped; } - private mapConversation(conversation: ConversationEntity): BridgeConversation { + private mapConversation(conversation: ConversationEntity): AgentConversation { return { identifier: conversation.identifier, status: conversation.status, @@ -328,7 +240,7 @@ export class BridgeExecutorService { }; } - private mapSubscriber(subscriber: SubscriberEntity | null): BridgeSubscriber | null { + private mapSubscriber(subscriber: SubscriberEntity | null): AgentSubscriber | null { if (!subscriber) { return null; } @@ -345,7 +257,7 @@ export class BridgeExecutorService { }; } - private mapReaction(reaction: BridgeReaction): BridgeReactionPayload { + private mapReaction(reaction: BridgeReaction): AgentReaction { return { messageId: reaction.messageId, emoji: { name: reaction.emoji }, @@ -354,7 +266,7 @@ export class BridgeExecutorService { }; } - private mapHistory(activities: ConversationActivityEntity[]): BridgeHistoryEntry[] { + private mapHistory(activities: ConversationActivityEntity[]): AgentHistoryEntry[] { return [...activities].reverse().map((activity) => ({ role: activity.senderType, type: activity.type, diff --git a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts index 8873f45a508..6ba3eee43e8 100644 --- a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts +++ b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts @@ -1,8 +1,11 @@ import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; +import type { Signal } from '@novu/framework'; import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; import { ReplyContentDto } from '../../dtos/agent-reply-payload.dto'; +export type { Signal } from '@novu/framework'; + export class HandleAgentReplyCommand extends EnvironmentWithUserCommand { @IsString() @IsNotEmpty() @@ -34,7 +37,3 @@ export class HandleAgentReplyCommand extends EnvironmentWithUserCommand { @IsArray() signals?: Signal[]; } - -export type Signal = - | { type: 'metadata'; key: string; value: unknown } - | { type: 'trigger'; workflowId: string; to?: string; payload?: Record }; diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index 2d22ce69993..fe8a6ff1daa 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -3,17 +3,32 @@ export { CronExpression } from './constants'; export { NovuRequestHandler, type ServeHandlerOptions } from './handler'; export type { Agent, + AgentAction, + AgentAttachment, + AgentBridgeRequest, AgentContext, + AgentConversation, AgentHandlers, + AgentHistoryEntry, + AgentMessage, + AgentMessageAuthor, + AgentPlatformContext, AgentReaction, + AgentReplyPayload, + AgentSubscriber, CardChild, CardElement, FileRef, MessageContent, + MetadataSignal, + ReplyContent, + Signal, + TriggerSignal, } from './resources'; export { Actions, agent, + AgentEventEnum, Button, Card, CardLink, diff --git a/packages/framework/src/resources/agent/index.ts b/packages/framework/src/resources/agent/index.ts index efad3dd9439..30eac7a88a9 100644 --- a/packages/framework/src/resources/agent/index.ts +++ b/packages/framework/src/resources/agent/index.ts @@ -30,6 +30,9 @@ export type { AgentSubscriber, FileRef, MessageContent, + MetadataSignal, ReplyContent, + Signal, + TriggerSignal, } from './agent.types'; export { AgentEventEnum } from './agent.types'; diff --git a/packages/shared/src/consts/providers/conversational-providers.ts b/packages/shared/src/consts/providers/conversational-providers.ts index c89ab4ab698..4107cf1af82 100644 --- a/packages/shared/src/consts/providers/conversational-providers.ts +++ b/packages/shared/src/consts/providers/conversational-providers.ts @@ -1,4 +1,4 @@ -import { ChatProviderIdEnum } from '../../types'; +import { ChatProviderIdEnum, InAppProviderIdEnum } from '../../types'; export type ConversationalProvider = { providerId: string; @@ -7,6 +7,7 @@ export type ConversationalProvider = { }; export const CONVERSATIONAL_PROVIDERS: ConversationalProvider[] = [ + { providerId: InAppProviderIdEnum.Novu, displayName: 'Dashboard' }, { providerId: ChatProviderIdEnum.Slack, displayName: 'Slack' }, { providerId: ChatProviderIdEnum.MsTeams, displayName: 'MS Teams', comingSoon: true }, { providerId: ChatProviderIdEnum.WhatsAppBusiness, displayName: 'WhatsApp Business' },