Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
23d3400
feat(dal): add Conversation and ConversationActivity entities
ChmaraX Apr 13, 2026
d1f2999
feat(api-service): add Chat SDK service, credential resolution, and w…
ChmaraX Apr 13, 2026
66045a2
fix(api-service): fix FeatureFlagContext type in guard
ChmaraX Apr 13, 2026
4716c25
fix(api-service): add missing ApiOperation import to agents controller
ChmaraX Apr 13, 2026
8e821f8
fix(api-service): use dynamic imports for ESM-only Chat SDK packages
ChmaraX Apr 13, 2026
a68a4dd
fix(api-service): use type imports from chat SDK instead of duplicati…
ChmaraX Apr 13, 2026
09f6fce
feat(api-service): add subscriber resolution for agent inbound messages
ChmaraX Apr 13, 2026
05de19c
chore: trigger CI
ChmaraX Apr 13, 2026
fa3bb47
fix(api-service): add missing platform-endpoint-config for subscriber…
ChmaraX Apr 13, 2026
9cd608a
fix(api-service): enable IS_CONVERSATIONAL_AGENTS_ENABLED in agents E…
ChmaraX Apr 13, 2026
4a4069f
feat(api-service): add conversation persistence for agent inbound mes…
ChmaraX Apr 13, 2026
3a032c4
fix(api-service): remove Telegram from agent platforms (no Novu provi…
ChmaraX Apr 13, 2026
6bb11e0
feat(dal): add messageCount, lastMessagePreview, and serializedThread…
ChmaraX Apr 14, 2026
9ad0af6
feat(api-service): add agentIdentifier to ResolvedPlatformConfig
ChmaraX Apr 14, 2026
ef673f3
feat(api-service): add event differentiation and thread serialization…
ChmaraX Apr 14, 2026
cb1bf65
feat(api-service): add bridge executor service for agent event dispatch
ChmaraX Apr 14, 2026
9c7c012
feat(api-service): add reply endpoint for agent message delivery
ChmaraX Apr 14, 2026
9adbfe2
feat(api-service): add metadata and resolve signal execution to reply…
ChmaraX Apr 14, 2026
5d1292d
refactor(api-service): clean up Phase 5/6 architecture
ChmaraX Apr 14, 2026
c7db3e3
refactor(api-service): replace JWT reply token with API key auth
ChmaraX Apr 14, 2026
97b5425
feat(api-service): add onResolve bridge callback and clean up executo…
ChmaraX Apr 14, 2026
2b888a0
fix(api-service): export nested DTOs to fix Swagger metadata generation
ChmaraX Apr 14, 2026
4495c3d
Merge branch 'next' into conversation-agents
ChmaraX Apr 14, 2026
37bde07
fix(api-service): E2E-verified fixes from agent testing
ChmaraX Apr 14, 2026
ee04877
fix(api-service): address PR review findings for agent feature
ChmaraX Apr 14, 2026
ab4740a
fix(api-service): add deliveryId to bridge payload for idempotent ret…
ChmaraX Apr 14, 2026
97d0f4f
fix(api-service): address remaining PR review findings
ChmaraX Apr 14, 2026
94b3978
fix(api-service): export SignalDto for OpenAPI metadata generation
ChmaraX Apr 14, 2026
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
5 changes: 5 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.971.0",
"@chat-adapter/slack": "^4.25.0",
"@chat-adapter/state-redis": "^4.25.0",
"@chat-adapter/teams": "^4.25.0",
"@chat-adapter/whatsapp": "^4.25.0",
"@godaddy/terminus": "^4.12.1",
"@google-cloud/storage": "^6.2.3",
"@nestjs/axios": "3.0.3",
Expand Down Expand Up @@ -82,6 +86,7 @@
"bcrypt": "^5.0.0",
"body-parser": "^2.2.1",
"bull": "^4.2.1",
"chat": "^4.25.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"clickhouse-migrations": "^1.2.0",
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const enterpriseImports = (): Array<Type | DynamicModule | Promise<DynamicModule
modules.push(require('@novu/ee-ai')?.AiModule);
}

if (require('@novu/ee-api')?.ConversationsModule) {
modules.push(require('@novu/ee-api')?.ConversationsModule);
}

modules.push(SupportModule);
modules.push(OutboundWebhooksModule.forRoot());
}
Expand Down
80 changes: 80 additions & 0 deletions apps/api/src/app/agents/agents-webhook.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
Body,
Controller,
HttpCode,
HttpException,
HttpStatus,
Param,
Post,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { UserSessionData } from '@novu/shared';
import { Request, Response } from 'express';
import { RequireAuthentication } from '../auth/framework/auth.decorator';
import { ExternalApiAccessible } from '../auth/framework/external-api.decorator';
import { UserSession } from '../shared/framework/user.decorator';
import { AgentReplyPayloadDto } from './dtos/agent-reply-payload.dto';
import { AgentConversationEnabledGuard } from './guards/agent-conversation-enabled.guard';
import { ChatSdkService } from './services/chat-sdk.service';
import { HandleAgentReplyCommand, Signal } from './usecases/handle-agent-reply/handle-agent-reply.command';
import { HandleAgentReply } from './usecases/handle-agent-reply/handle-agent-reply.usecase';

@Controller('/agents')
@UseGuards(AgentConversationEnabledGuard)
@ApiExcludeController()
export class AgentsWebhookController {
constructor(
private chatSdkService: ChatSdkService,
private handleAgentReplyUsecase: HandleAgentReply
) {}

@Post('/:agentId/reply')
@HttpCode(HttpStatus.OK)
@RequireAuthentication()
@ExternalApiAccessible()
async handleAgentReply(
@UserSession() user: UserSessionData,
@Param('agentId') agentId: string,
@Body() body: AgentReplyPayloadDto
) {
return this.handleAgentReplyUsecase.execute(
HandleAgentReplyCommand.create({
userId: user._id,
environmentId: user.environmentId,
organizationId: user.organizationId,
conversationId: body.conversationId,
agentIdentifier: agentId,
integrationIdentifier: body.integrationIdentifier,
reply: body.reply,
update: body.update,
resolve: body.resolve,
signals: body.signals as Signal[],
})
);
}

@Post('/:agentId/webhook/:integrationIdentifier')
@HttpCode(HttpStatus.OK)
async handleInboundWebhook(
@Param('agentId') agentId: string,
@Param('integrationIdentifier') integrationIdentifier: string,
@Req() req: Request,
@Res() 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 {
res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: 'Internal server error' });
}
}
}
}
7 changes: 5 additions & 2 deletions apps/api/src/app/agents/agents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
Patch,
Post,
Query,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiExcludeController, ApiOperation } from '@nestjs/swagger';
import { RequirePermissions } from '@novu/application-generic';
import { ApiRateLimitCategoryEnum, DirectionEnum, PermissionsEnum, UserSessionData } from '@novu/shared';
import { RequireAuthentication } from '../auth/framework/auth.decorator';
Expand All @@ -36,6 +37,7 @@ import {
UpdateAgentIntegrationRequestDto,
UpdateAgentRequestDto,
} from './dtos';
import { AgentConversationEnabledGuard } from './guards/agent-conversation-enabled.guard';
import { AddAgentIntegrationCommand } from './usecases/add-agent-integration/add-agent-integration.command';
import { AddAgentIntegration } from './usecases/add-agent-integration/add-agent-integration.usecase';
import { CreateAgentCommand } from './usecases/create-agent/create-agent.command';
Expand All @@ -59,7 +61,8 @@ import { UpdateAgent } from './usecases/update-agent/update-agent.usecase';
@ApiCommonResponses()
@Controller('/agents')
@UseInterceptors(ClassSerializerInterceptor)
@ApiTags('Agents')
@UseGuards(AgentConversationEnabledGuard)
@ApiExcludeController()
@RequireAuthentication()
export class AgentsController {
constructor(
Expand Down
31 changes: 28 additions & 3 deletions apps/api/src/app/agents/agents.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import { Module } from '@nestjs/common';
import {
ChannelConnectionRepository,
ChannelEndpointRepository,
ConversationActivityRepository,
ConversationRepository,
} from '@novu/dal';

import { AuthModule } from '../auth/auth.module';
import { SharedModule } from '../shared/shared.module';
import { AgentsController } from './agents.controller';
import { AgentsWebhookController } from './agents-webhook.controller';
import { AgentConversationService } from './services/agent-conversation.service';
import { AgentCredentialService } from './services/agent-credential.service';
import { AgentInboundHandler } from './services/agent-inbound-handler.service';
import { AgentSubscriberResolver } from './services/agent-subscriber-resolver.service';
import { BridgeExecutorService } from './services/bridge-executor.service';
import { ChatSdkService } from './services/chat-sdk.service';
import { USE_CASES } from './usecases';

@Module({
imports: [SharedModule, AuthModule],
controllers: [AgentsController],
providers: [...USE_CASES],
exports: [...USE_CASES],
controllers: [AgentsController, AgentsWebhookController],
providers: [
...USE_CASES,
ChannelConnectionRepository,
ChannelEndpointRepository,
ConversationRepository,
ConversationActivityRepository,
AgentCredentialService,
AgentSubscriberResolver,
AgentConversationService,
AgentInboundHandler,
BridgeExecutorService,
ChatSdkService,
],
exports: [...USE_CASES, ChatSdkService],
})
export class AgentsModule {}
6 changes: 6 additions & 0 deletions apps/api/src/app/agents/dtos/agent-event.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum AgentEventEnum {
ON_START = 'onStart',
ON_MESSAGE = 'onMessage',
ON_ACTION = 'onAction',
ON_RESOLVE = 'onResolve',
}
5 changes: 5 additions & 0 deletions apps/api/src/app/agents/dtos/agent-platform.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum AgentPlatformEnum {
SLACK = 'slack',
WHATSAPP = 'whatsapp',
TEAMS = 'teams',
}
67 changes: 67 additions & 0 deletions apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsIn, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator';

const SIGNAL_TYPES = ['metadata', 'trigger'] as const;

export class TextContentDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
@MaxLength(40_000)
text: string;
}

export class ResolveDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
summary?: string;
}

export class SignalDto {
@ApiProperty({ enum: SIGNAL_TYPES })
@IsString()
@IsIn(SIGNAL_TYPES)
type: (typeof SIGNAL_TYPES)[number];
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SignalDto is missing key and value fields for metadata signals.

The use-case at handle-agent-reply.usecase.ts:144 expects metadata signals to have { type: 'metadata'; key: string; value: unknown }, but this DTO only validates type. Incoming metadata signals won't have key/value validated, allowing malformed payloads to reach the use-case.

Consider using a discriminated union or separate DTOs per signal type:

Proposed fix
+export class MetadataSignalDto {
+  `@ApiProperty`({ enum: ['metadata'] })
+  `@IsString`()
+  `@IsIn`(['metadata'])
+  type: 'metadata';
+
+  `@ApiProperty`()
+  `@IsString`()
+  `@IsNotEmpty`()
+  key: string;
+
+  `@ApiProperty`()
+  value: unknown;
+}
+
+export class TriggerSignalDto {
+  `@ApiProperty`({ enum: ['trigger'] })
+  `@IsString`()
+  `@IsIn`(['trigger'])
+  type: 'trigger';
+
+  // Add trigger-specific fields as needed
+}

-export class SignalDto {
-  `@ApiProperty`({ enum: SIGNAL_TYPES })
-  `@IsString`()
-  `@IsIn`(SIGNAL_TYPES)
-  type: (typeof SIGNAL_TYPES)[number];
-}

Then update AgentReplyPayloadDto.signals to accept an array of MetadataSignalDto | TriggerSignalDto.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/agents/dtos/agent-reply-payload.dto.ts` around lines 22 -
27, SignalDto only validates type and lacks required key/value fields for
metadata signals, so create separate DTOs (e.g., MetadataSignalDto with type:
'metadata', `@IsString`() key, and value typed/validated as unknown or a safe
persisted type, and TriggerSignalDto for other signal types) or implement a
discriminated-union DTO pattern, then change AgentReplyPayloadDto.signals to
accept an array of MetadataSignalDto | TriggerSignalDto; ensure the SIGNAL_TYPES
enum validation remains and the metadata branch matches the shape expected by
the handler (handle-agent-reply use case) that expects { type: 'metadata'; key:
string; value: unknown } so malformed payloads are rejected by validation.


export class AgentReplyPayloadDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
conversationId: string;

@ApiProperty()
@IsString()
@IsNotEmpty()
integrationIdentifier: string;

@ApiPropertyOptional({ type: TextContentDto })
@IsOptional()
@IsObject()
@ValidateNested()
@Type(() => TextContentDto)
reply?: TextContentDto;

@ApiPropertyOptional({ type: TextContentDto })
@IsOptional()
@IsObject()
@ValidateNested()
@Type(() => TextContentDto)
update?: TextContentDto;

@ApiPropertyOptional({ type: ResolveDto })
@IsOptional()
@IsObject()
@ValidateNested()
@Type(() => ResolveDto)
resolve?: ResolveDto;

@ApiPropertyOptional({ type: [SignalDto] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => SignalDto)
signals?: SignalDto[];
}
4 changes: 4 additions & 0 deletions apps/api/src/app/agents/e2e/agents.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ describe('Agents API - /agents #novu-v2', () => {
const agentRepository = new AgentRepository();
const agentIntegrationRepository = new AgentIntegrationRepository();

before(() => {
process.env.IS_CONVERSATIONAL_AGENTS_ENABLED = 'true';
});

beforeEach(async () => {
session = new UserSession();
await session.initialize();
Expand Down
26 changes: 26 additions & 0 deletions apps/api/src/app/agents/guards/agent-conversation-enabled.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CanActivate, ExecutionContext, Injectable, NotFoundException } from '@nestjs/common';
import { FeatureFlagsService } from '@novu/application-generic';
import { FeatureFlagsKeysEnum, UserSessionData } from '@novu/shared';

@Injectable()
export class AgentConversationEnabledGuard implements CanActivate {
constructor(private readonly featureFlagsService: FeatureFlagsService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user: UserSessionData | undefined = request.user;

const isEnabled = await this.featureFlagsService.getFlag({
key: FeatureFlagsKeysEnum.IS_CONVERSATIONAL_AGENTS_ENABLED,
defaultValue: false,
organization: { _id: user?.organizationId ?? '' },
environment: { _id: user?.environmentId ?? '' },
});

if (!isEnabled) {
throw new NotFoundException();
}

return true;
}
}
Loading
Loading