-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat(api-service): Conversational Agents — full inbound/outbound pipeline with bridge executor fixes NV-7346 #10692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 d1f2999
feat(api-service): add Chat SDK service, credential resolution, and w…
ChmaraX 66045a2
fix(api-service): fix FeatureFlagContext type in guard
ChmaraX 4716c25
fix(api-service): add missing ApiOperation import to agents controller
ChmaraX 8e821f8
fix(api-service): use dynamic imports for ESM-only Chat SDK packages
ChmaraX a68a4dd
fix(api-service): use type imports from chat SDK instead of duplicati…
ChmaraX 09f6fce
feat(api-service): add subscriber resolution for agent inbound messages
ChmaraX 05de19c
chore: trigger CI
ChmaraX fa3bb47
fix(api-service): add missing platform-endpoint-config for subscriber…
ChmaraX 9cd608a
fix(api-service): enable IS_CONVERSATIONAL_AGENTS_ENABLED in agents E…
ChmaraX 4a4069f
feat(api-service): add conversation persistence for agent inbound mes…
ChmaraX 3a032c4
fix(api-service): remove Telegram from agent platforms (no Novu provi…
ChmaraX 6bb11e0
feat(dal): add messageCount, lastMessagePreview, and serializedThread…
ChmaraX 9ad0af6
feat(api-service): add agentIdentifier to ResolvedPlatformConfig
ChmaraX ef673f3
feat(api-service): add event differentiation and thread serialization…
ChmaraX cb1bf65
feat(api-service): add bridge executor service for agent event dispatch
ChmaraX 9c7c012
feat(api-service): add reply endpoint for agent message delivery
ChmaraX 9adbfe2
feat(api-service): add metadata and resolve signal execution to reply…
ChmaraX 5d1292d
refactor(api-service): clean up Phase 5/6 architecture
ChmaraX c7db3e3
refactor(api-service): replace JWT reply token with API key auth
ChmaraX 97b5425
feat(api-service): add onResolve bridge callback and clean up executo…
ChmaraX 2b888a0
fix(api-service): export nested DTOs to fix Swagger metadata generation
ChmaraX 4495c3d
Merge branch 'next' into conversation-agents
ChmaraX 37bde07
fix(api-service): E2E-verified fixes from agent testing
ChmaraX ee04877
fix(api-service): address PR review findings for agent feature
ChmaraX ab4740a
fix(api-service): add deliveryId to bridge payload for idempotent ret…
ChmaraX 97d0f4f
fix(api-service): address remaining PR review findings
ChmaraX 94b3978
fix(api-service): export SignalDto for OpenAPI metadata generation
ChmaraX File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' }); | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export enum AgentPlatformEnum { | ||
| SLACK = 'slack', | ||
| WHATSAPP = 'whatsapp', | ||
| TEAMS = 'teams', | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | ||
| } | ||
|
|
||
| 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[]; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
apps/api/src/app/agents/guards/agent-conversation-enabled.guard.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SignalDtois missingkeyandvaluefields for metadata signals.The use-case at
handle-agent-reply.usecase.ts:144expects metadata signals to have{ type: 'metadata'; key: string; value: unknown }, but this DTO only validatestype. Incoming metadata signals won't havekey/valuevalidated, allowing malformed payloads to reach the use-case.Consider using a discriminated union or separate DTOs per signal type:
Proposed fix
Then update
AgentReplyPayloadDto.signalsto accept an array ofMetadataSignalDto | TriggerSignalDto.🤖 Prompt for AI Agents