Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions apps/api/src/app/agents/dtos/agent-event.enum.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
export enum AgentEventEnum {
ON_MESSAGE = 'onMessage',
ON_ACTION = 'onAction',
ON_RESOLVE = 'onResolve',
ON_REACTION = 'onReaction',
}
export { AgentEventEnum } from '@novu/framework';
28 changes: 23 additions & 5 deletions apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -146,6 +163,7 @@ export class AgentReplyPayloadDto {
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Validate(IsValidSignal, { each: true })
@Type(() => SignalDto)
signals?: SignalDto[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down Expand Up @@ -263,7 +259,7 @@ export class AgentInboundHandler {
agentId: string,
config: ResolvedAgentConfig,
thread: Thread,
action: BridgeAction,
action: AgentAction,
userId: string
): Promise<void> {
const subscriberId = await this.subscriberResolver
Expand Down
130 changes: 21 additions & 109 deletions apps/api/src/app/agents/services/bridge-executor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string, unknown>;
messageCount: number;
createdAt: string;
lastActivityAt: string;
}

interface BridgeSubscriber {
subscriberId: string;
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
avatar?: string;
locale?: string;
data?: Record<string, unknown>;
}

interface BridgeHistoryEntry {
role: ConversationActivitySenderTypeEnum;
type: ConversationActivityTypeEnum;
content: string;
richContent?: Record<string, unknown>;
senderName?: string;
signalData?: { type: string; payload?: Record<string, unknown> };
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}`);
Expand Down Expand Up @@ -269,7 +183,7 @@ export class BridgeExecutorService {
deliveryId = `${conversation._id}:${event}`;
}

const payload: AgentBridgeRequest = {
return {
version: 1,
timestamp,
deliveryId,
Expand All @@ -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: {
Expand All @@ -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,
Expand All @@ -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;
}
Expand All @@ -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 },
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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<string, unknown> };
15 changes: 15 additions & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/framework/src/resources/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export type {
AgentSubscriber,
FileRef,
MessageContent,
MetadataSignal,
ReplyContent,
Signal,
TriggerSignal,
} from './agent.types';
export { AgentEventEnum } from './agent.types';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatProviderIdEnum } from '../../types';
import { ChatProviderIdEnum, InAppProviderIdEnum } from '../../types';

export type ConversationalProvider = {
providerId: string;
Expand All @@ -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' },
Expand Down
Loading