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
16 changes: 15 additions & 1 deletion apps/api/src/app/agents/agents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { DeleteAgentCommand } from './usecases/delete-agent/delete-agent.command
import { DeleteAgent } from './usecases/delete-agent/delete-agent.usecase';
import { GetAgentCommand } from './usecases/get-agent/get-agent.command';
import { GetAgent } from './usecases/get-agent/get-agent.usecase';
import { type AgentEmojiEntry, ListAgentEmoji } from './usecases/list-agent-emoji/list-agent-emoji.usecase';
import { ListAgentIntegrationsCommand } from './usecases/list-agent-integrations/list-agent-integrations.command';
import { ListAgentIntegrations } from './usecases/list-agent-integrations/list-agent-integrations.usecase';
import { ListAgentsCommand } from './usecases/list-agents/list-agents.command';
Expand Down Expand Up @@ -77,9 +78,22 @@ export class AgentsController {
private readonly addAgentIntegrationUsecase: AddAgentIntegration,
private readonly listAgentIntegrationsUsecase: ListAgentIntegrations,
private readonly updateAgentIntegrationUsecase: UpdateAgentIntegration,
private readonly removeAgentIntegrationUsecase: RemoveAgentIntegration
private readonly removeAgentIntegrationUsecase: RemoveAgentIntegration,
private readonly listAgentEmojiUsecase: ListAgentEmoji
) {}

@Get('/emoji')
@ApiOperation({
summary: 'List available emoji',
description:
'Returns the set of well-known cross-platform emoji names supported for agent reactions. ' +
'Each entry includes the normalized name and a unicode representation for display.',
})
@RequirePermissions(PermissionsEnum.AGENT_READ)
listAgentEmoji(): Promise<AgentEmojiEntry[]> {
return this.listAgentEmojiUsecase.execute();
}
Comment on lines +85 to +95
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.

🛠️ Refactor suggestion | 🟠 Major

Method name and missing @ApiResponse decorator.

Two guideline deviations:

  1. The method name listEmoji doesn't match the list{EntityName} pattern used elsewhere in this controller (listAgents, listAgentIntegrations). Rename to listAgentEmoji for consistency.
  2. The endpoint lacks a return type annotation and @ApiResponse decorator. Even though the controller is @ApiExcludeController, every other endpoint here declares one and it documents the response shape for generated clients/dashboards.
Proposed change
-  `@Get`('/emoji')
-  `@ApiOperation`({
-    summary: 'List available emoji',
-    description:
-      'Returns the set of well-known cross-platform emoji names supported for agent reactions. ' +
-      'Each entry includes the normalized name and a unicode representation for display.',
-  })
-  `@RequirePermissions`(PermissionsEnum.AGENT_READ)
-  listEmoji() {
-    return this.listAgentEmojiUsecase.execute();
-  }
+  `@Get`('/emoji')
+  `@ApiOperation`({
+    summary: 'List available emoji',
+    description:
+      'Returns the set of well-known cross-platform emoji names supported for agent reactions. ' +
+      'Each entry includes the normalized name and a unicode representation for display.',
+  })
+  `@RequirePermissions`(PermissionsEnum.AGENT_READ)
+  listAgentEmoji(): Promise<AgentEmojiEntry[]> {
+    return this.listAgentEmojiUsecase.execute();
+  }

As per coding guidelines: "Controller method names follow the pattern: getEntityName, listEntityName, ..." and "Every endpoint must have @ApiOperation, @ApiResponse, and @ApiTags decorators for OpenAPI documentation".

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

In `@apps/api/src/app/agents/agents.controller.ts` around lines 85 - 95, Rename
the controller method listEmoji to listAgentEmoji for consistency, update its
call to use listAgentEmojiUsecase.execute(), add an explicit return type (e.g.,
Promise<AgentEmojiDto[]> or the existing DTO type used elsewhere), and annotate
the method with an `@ApiResponse`(...) decorator describing the 200 response
schema and type; keep the existing `@ApiOperation` and
`@RequirePermissions`(PermissionsEnum.AGENT_READ) intact. Ensure the `@ApiResponse`
references the same DTO type used in other endpoints so generated clients/docs
pick up the response shape.


@Post('/')
@ApiResponse(AgentResponseDto, 201)
@ApiOperation({
Expand Down
15 changes: 10 additions & 5 deletions apps/api/src/app/agents/dtos/agent-behavior.dto.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator';
import { IsBoolean, IsOptional, ValidateIf, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { IsWellKnownEmoji } from '../validators/is-well-known-emoji.validator';

export class AgentReactionSettingsDto {
@ApiPropertyOptional({
description: 'Emoji reaction for incoming messages. Emoji name string to customize, null to disable. Default: "eyes" (👀)',
description:
'Cross-platform emoji name for incoming messages (e.g. "eyes", "thumbs_up"). ' +
'Set to null to disable. Default: "eyes"',
default: 'eyes',
})
@IsOptional()
@ValidateIf((_, value) => value !== null)
@IsString()
@IsWellKnownEmoji()
onMessageReceived?: string | null;

@ApiPropertyOptional({
description: 'Emoji reaction when a conversation is resolved. Emoji name string to customize, null to disable. Default: "check" (✅)',
description:
'Cross-platform emoji name for resolved conversations (e.g. "check", "star"). ' +
'Set to null to disable. Default: "check"',
default: 'check',
})
@IsOptional()
@ValidateIf((_, value) => value !== null)
@IsString()
@IsWellKnownEmoji()
onResolved?: string | null;
}

Expand Down
15 changes: 10 additions & 5 deletions apps/api/src/app/agents/e2e/agent-webhook.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { testServer } from '@novu/testing';
import { expect } from 'chai';
import sinon from 'sinon';
import type { EmojiValue } from 'chat';
import { AgentEventEnum } from '../dtos/agent-event.enum';
import { AgentConfigResolver } from '../services/agent-config-resolver.service';
import { AgentInboundHandler, InboundReactionEvent } from '../services/agent-inbound-handler.service';
Expand All @@ -20,6 +21,10 @@ import {
} from './helpers/agent-test-setup';
import { buildSlackChallenge, signSlackRequest } from './helpers/providers/slack';

function mockEmoji(name: string): EmojiValue {
return { name, toJSON: () => `{{emoji:${name}}}`, toString: () => `{{emoji:${name}}}` };
}

function mockSentMessage() {
return {
addReaction: async () => {},
Expand Down Expand Up @@ -359,7 +364,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => {
bridgeCalls = [];

const reactionEvent: InboundReactionEvent = {
emoji: { name: 'thumbs_up' },
emoji: mockEmoji('thumbs_up'),
added: true,
messageId: msg.id,
message: msg as any,
Expand All @@ -379,7 +384,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => {

it('should skip reaction when no conversation exists for the thread', async () => {
const reactionEvent: InboundReactionEvent = {
emoji: { name: 'wave' },
emoji: mockEmoji('wave'),
added: true,
messageId: 'msg-orphan',
thread: mockThread(`T_NOCONV_${Date.now()}`) as any,
Expand All @@ -392,7 +397,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => {

it('should skip reaction when thread context is missing', async () => {
const reactionEvent: InboundReactionEvent = {
emoji: { name: 'fire' },
emoji: mockEmoji('fire'),
added: false,
messageId: 'msg-no-thread',
};
Expand All @@ -410,7 +415,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => {
bridgeCalls = [];

const reactionEvent: InboundReactionEvent = {
emoji: { name: 'tada' },
emoji: mockEmoji('tada'),
added: true,
messageId: msg.id,
message: msg as any,
Expand Down Expand Up @@ -443,7 +448,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => {
);

const reactionEvent: InboundReactionEvent = {
emoji: { name: 'heart' },
emoji: mockEmoji('heart'),
added: true,
messageId: msg.id,
message: msg as any,
Expand Down
47 changes: 38 additions & 9 deletions apps/api/src/app/agents/services/agent-config-resolver.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,22 @@ import {
IntegrationRepository,
} from '@novu/dal';
import { FeatureFlagsKeysEnum } from '@novu/shared';
import type { WellKnownEmoji } from 'chat';
import { AgentPlatformEnum } from '../dtos/agent-platform.enum';
import { esmImport } from '../utils/esm-import';
import { resolveAgentPlatform } from '../utils/provider-to-platform';

let cachedEmojiNames: Set<string> | null = null;

async function loadEmojiNames(): Promise<Set<string>> {
if (cachedEmojiNames) return cachedEmojiNames;

const { DEFAULT_EMOJI_MAP } = await esmImport('chat');
cachedEmojiNames = new Set<string>(Object.keys(DEFAULT_EMOJI_MAP));

return cachedEmojiNames;
}

export interface ResolvedAgentConfig {
platform: AgentPlatformEnum;
credentials: ICredentialsEntity;
Expand All @@ -21,25 +34,36 @@ export interface ResolvedAgentConfig {
integrationIdentifier: string;
integrationId: string;
thinkingIndicatorEnabled: boolean;
reactionOnMessageReceived: string | null;
reactionOnResolved: string | null;
reactionOnMessageReceived: WellKnownEmoji | null;
reactionOnResolved: WellKnownEmoji | null;
bridgeUrl?: string;
devBridgeUrl?: string;
devBridgeActive?: boolean;
}

const DEFAULT_REACTION_ON_MESSAGE = 'eyes';
const DEFAULT_REACTION_ON_RESOLVED = 'check';
const DEFAULT_REACTION_ON_MESSAGE: WellKnownEmoji = 'eyes';
const DEFAULT_REACTION_ON_RESOLVED: WellKnownEmoji = 'check';

function resolveThinkingIndicator(agent: { behavior?: { thinkingIndicatorEnabled?: boolean } }): boolean {
return agent.behavior?.thinkingIndicatorEnabled !== false;
}

function resolveReaction(value: string | null | undefined, defaultEmoji: string): string | null {
async function resolveReaction(
value: string | null | undefined,
defaultEmoji: WellKnownEmoji,
log: PinoLogger
): Promise<WellKnownEmoji | null> {
if (value === null) return null;
if (value === undefined) return defaultEmoji;

return value;
const known = await loadEmojiNames();
if (!known.has(value)) {
log.warn(`Unknown emoji "${value}" in agent config, falling back to default "${defaultEmoji}"`);

return defaultEmoji;
}

return value as WellKnownEmoji;
}

@Injectable()
Expand Down Expand Up @@ -133,11 +157,16 @@ export class AgentConfigResolver {
integrationIdentifier,
integrationId: integration._id,
thinkingIndicatorEnabled: resolveThinkingIndicator(agent),
reactionOnMessageReceived: resolveReaction(
reactionOnMessageReceived: await resolveReaction(
agent.behavior?.reactions?.onMessageReceived,
DEFAULT_REACTION_ON_MESSAGE
DEFAULT_REACTION_ON_MESSAGE,
this.logger
),
reactionOnResolved: await resolveReaction(
agent.behavior?.reactions?.onResolved,
DEFAULT_REACTION_ON_RESOLVED,
this.logger
),
reactionOnResolved: resolveReaction(agent.behavior?.reactions?.onResolved, DEFAULT_REACTION_ON_RESOLVED),
bridgeUrl: agent.bridgeUrl,
devBridgeUrl: agent.devBridgeUrl,
devBridgeActive: agent.devBridgeActive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ConversationRepository,
SubscriberRepository,
} from '@novu/dal';
import type { Message, Thread } from 'chat';
import type { EmojiValue, 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';
Expand All @@ -22,7 +22,7 @@ const ONBOARDING_NO_BRIDGE_REPLY_MARKDOWN = `*You're connected to Novu*
Your bot is linked successfully. Go back to the *Novu dashboard* to complete onboarding.`;

export interface InboundReactionEvent {
emoji: { name: string };
emoji: EmojiValue;
added: boolean;
messageId: string;
message?: Message;
Expand Down
32 changes: 20 additions & 12 deletions apps/api/src/app/agents/services/chat-sdk.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { BadRequestException, forwardRef, Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
import { PinoLogger } from '@novu/application-generic';
import type { Chat, Message, Thread } from 'chat';
import type { Chat, EmojiValue, Message, ReactionEvent, Thread } from 'chat';
import { Request as ExpressRequest, Response as ExpressResponse } from 'express';
import { LRUCache } from 'lru-cache';
import { AgentEventEnum } from '../dtos/agent-event.enum';
import { AgentPlatformEnum } from '../dtos/agent-platform.enum';
import type { ReplyContentDto } from '../dtos/agent-reply-payload.dto';
import { esmImport } from '../utils/esm-import';
import { sendWebResponse, toWebRequest } from '../utils/express-to-web-request';
import { AgentConfigResolver, ResolvedAgentConfig } from './agent-config-resolver.service';
import { AgentInboundHandler } from './agent-inbound-handler.service';
Expand All @@ -26,11 +27,6 @@ import { AgentInboundHandler } from './agent-inbound-handler.service';
* credentials.phoneNumberIdentification → phoneNumberId
*/

// Chat SDK packages are ESM-only; SWC rewrites import() → require() for CJS output.
// Wrapping in new Function prevents SWC from seeing the import() keyword.
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const esmImport = new Function('specifier', 'return import(specifier)') as (s: string) => Promise<any>;

const MAX_CACHED_INSTANCES = 200;
const INSTANCE_TTL_MS = 1000 * 60 * 30;

Expand Down Expand Up @@ -133,14 +129,15 @@ export class ChatSdkService implements OnModuleDestroy {
platform: string,
platformThreadId: string,
platformMessageId: string,
emoji: string
emojiName: string
): Promise<void> {
const config = await this.agentConfigResolver.resolve(agentId, integrationIdentifier);
const instanceKey = `${agentId}:${integrationIdentifier}`;
const chat = await this.getOrCreate(instanceKey, agentId, config.platform, config);

const adapter = chat.getAdapter(platform);
await adapter.removeReaction(platformThreadId, platformMessageId, emoji);
const resolved = await this.resolveEmoji(emojiName);
await adapter.removeReaction(platformThreadId, platformMessageId, resolved);
}

async reactToMessage(
Expand All @@ -149,14 +146,25 @@ export class ChatSdkService implements OnModuleDestroy {
platform: string,
platformThreadId: string,
platformMessageId: string,
emoji: string
emojiName: string
): Promise<void> {
const config = await this.agentConfigResolver.resolve(agentId, integrationIdentifier);
const instanceKey = `${agentId}:${integrationIdentifier}`;
const chat = await this.getOrCreate(instanceKey, agentId, config.platform, config);

const adapter = chat.getAdapter(platform);
await adapter.addReaction(platformThreadId, platformMessageId, emoji);
const resolved = await this.resolveEmoji(emojiName);
await adapter.addReaction(platformThreadId, platformMessageId, resolved);
}

private async resolveEmoji(name: string): Promise<EmojiValue> {
const { getEmoji } = await esmImport('chat');
const resolved = getEmoji(name);
if (!resolved) {
throw new Error(`Unknown emoji name: "${name}". Use GET /agents/emoji to list supported options.`);
}

return resolved;
}
Comment on lines +160 to 168
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether esmImport memoizes its result (so per-call overhead is a no-op after first load).
fd -t f 'esm-import.ts' | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Repository: novuhq/novu

Length of output: 440


🏁 Script executed:

# Check if BadRequestException is imported in the file
grep -n "BadRequestException\|import.*Exception" apps/api/src/app/agents/services/chat-sdk.service.ts | head -20

Repository: novuhq/novu

Length of output: 633


🏁 Script executed:

# Find resolveEmoji call sites to understand frequency and context
rg "resolveEmoji" apps/api/src/app/agents/services/ -A 2 -B 2

Repository: novuhq/novu

Length of output: 1330


🏁 Script executed:

# Look for the `@IsWellKnownEmoji` validator definition
fd -t f . apps/api | xargs rg "@IsWellKnownEmoji|IsWellKnownEmoji" -l

Repository: novuhq/novu

Length of output: 173


🏁 Script executed:

# Check the entire chat-sdk.service.ts file to understand the structure and caching patterns
wc -l apps/api/src/app/agents/services/chat-sdk.service.ts

Repository: novuhq/novu

Length of output: 111


🏁 Script executed:

# Search for any existing emoji module caching at module level
rg "chat.*emoji|emoji.*chat" apps/api/src/app/agents/services/chat-sdk.service.ts -B 2 -A 2

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check the `@IsWellKnownEmoji` validator implementation
cat -n apps/api/src/app/agents/validators/is-well-known-emoji.validator.ts

Repository: novuhq/novu

Length of output: 1718


🏁 Script executed:

# Look at the removeReaction and addReaction methods to understand the full flow
rg "removeReaction|addReaction" apps/api/src/app/agents/services/chat-sdk.service.ts -B 5 -A 5

Repository: novuhq/novu

Length of output: 1206


🏁 Script executed:

# Check for DTO usage to see where `@IsWellKnownEmoji` is applied
rg "@IsWellKnownEmoji" apps/api/src/app/agents/dtos/ -B 2 -A 2

Repository: novuhq/novu

Length of output: 801


Use BadRequestException for invalid emoji names and consider caching the emoji module to avoid repeated ESM resolution on hot paths.

Two confirmed issues in resolveEmoji:

  1. Throwing a plain Error surfaces as a 500 instead of a 400. Since the rejection is due to an invalid client-supplied value, BadRequestException is semantically correct and consistent with the pattern used throughout this file (lines 63, 236, 250, 265, 282). The @IsWellKnownEmoji validator on the DTO provides defense-in-depth, but programmatic callers and SDK-version mismatches can still reach this path.

  2. resolveEmoji is awaited on every reactToMessage and removeReaction call. The esmImport utility has no memoization—it's a thin wrapper around import()—so each call incurs ESM resolution overhead on these hot paths. While the validator already caches emoji names, resolveEmoji should cache the getEmoji function or be lifted to a module-level reference.

Suggested fix for error handling
-  private async resolveEmoji(name: string): Promise<EmojiValue> {
-    const { getEmoji } = await esmImport('chat');
-    const resolved = getEmoji(name);
-    if (!resolved) {
-      throw new Error(`Unknown emoji name: "${name}". Use GET /agents/emoji to list supported options.`);
-    }
-
-    return resolved;
-  }
+  private async resolveEmoji(name: string): Promise<EmojiValue> {
+    const { getEmoji } = await esmImport('chat');
+    const resolved = getEmoji(name);
+    if (!resolved) {
+      throw new BadRequestException(
+        `Unknown emoji name: "${name}". Use GET /agents/emoji to list supported options.`
+      );
+    }
+
+    return resolved;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/agents/services/chat-sdk.service.ts` around lines 154 - 162,
Replace the plain Error thrown in resolveEmoji with Nest's BadRequestException
(imported from `@nestjs/common`) so invalid emoji names produce HTTP 400; also
avoid repeated ESM resolution by caching the imported getEmoji function (either
memoize esmImport result or lift getEmoji to a module-level variable) so
resolveEmoji uses the cached getEmoji on subsequent calls; update resolveEmoji,
and ensure callers reactToMessage and removeReaction benefit from the cached
function while keeping the `@IsWellKnownEmoji` validator as defense-in-depth.


private async getOrCreate(
Expand Down Expand Up @@ -379,14 +387,14 @@ export class ChatSdkService implements OnModuleDestroy {
}
});

cached.chat.onReaction(async (event: any) => {
cached.chat.onReaction(async (event: ReactionEvent) => {
try {
await this.inboundHandler.handleReaction(agentId, cached.config, {
emoji: event.emoji,
added: event.added,
messageId: event.messageId,
message: event.message,
thread: event.thread,
thread: event.thread as Thread | undefined,
user: event.user,
});
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Signal } from '@novu/framework';
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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class HandleAgentReply {
}

if (command.resolve) {
await this.executeResolveSignal(command, config!, conversation, channel, command.resolve);
await this.resolveConversation(command, config!, conversation, channel, command.resolve);
}

return { status: 'ok' };
Expand Down Expand Up @@ -255,12 +255,12 @@ export class HandleAgentReply {
]);
}

private async executeResolveSignal(
private async resolveConversation(
command: HandleAgentReplyCommand,
config: ResolvedAgentConfig,
conversation: ConversationEntity,
channel: ConversationChannel,
signal: { summary?: string }
options: { summary?: string }
): Promise<void> {
await Promise.all([
this.conversationRepository.updateStatus(
Expand All @@ -276,8 +276,8 @@ export class HandleAgentReply {
integrationId: channel._integrationId,
platformThreadId: channel.platformThreadId,
agentId: command.agentIdentifier,
content: signal.summary ?? 'Conversation resolved',
signalData: { type: 'resolve', payload: signal.summary ? { summary: signal.summary } : undefined },
content: options.summary ?? 'Conversation resolved',
signalData: { type: 'resolve', payload: options.summary ? { summary: options.summary } : undefined },
environmentId: command.environmentId,
organizationId: command.organizationId,
}),
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app/agents/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CreateAgent } from './create-agent/create-agent.usecase';
import { DeleteAgent } from './delete-agent/delete-agent.usecase';
import { GetAgent } from './get-agent/get-agent.usecase';
import { HandleAgentReply } from './handle-agent-reply/handle-agent-reply.usecase';
import { ListAgentEmoji } from './list-agent-emoji/list-agent-emoji.usecase';
import { ListAgentIntegrations } from './list-agent-integrations/list-agent-integrations.usecase';
import { ListAgents } from './list-agents/list-agents.usecase';
import { RemoveAgentIntegration } from './remove-agent-integration/remove-agent-integration.usecase';
Expand All @@ -16,6 +17,7 @@ export const USE_CASES = [
UpdateAgent,
DeleteAgent,
AddAgentIntegration,
ListAgentEmoji,
ListAgentIntegrations,
UpdateAgentIntegration,
RemoveAgentIntegration,
Expand Down
Loading
Loading