diff --git a/apps/api/src/app/agents/agents.controller.ts b/apps/api/src/app/agents/agents.controller.ts index 5d813d29765..3cb76dac154 100644 --- a/apps/api/src/app/agents/agents.controller.ts +++ b/apps/api/src/app/agents/agents.controller.ts @@ -287,6 +287,7 @@ export class AgentsController { identifier, name: body.name, description: body.description, + behavior: body.behavior, }) ); } diff --git a/apps/api/src/app/agents/dtos/agent-behavior.dto.ts b/apps/api/src/app/agents/dtos/agent-behavior.dto.ts new file mode 100644 index 00000000000..e2b0cd33395 --- /dev/null +++ b/apps/api/src/app/agents/dtos/agent-behavior.dto.ts @@ -0,0 +1,9 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class AgentBehaviorDto { + @ApiPropertyOptional({ description: 'Show a "Thinking..." indicator while the agent is processing a message' }) + @IsBoolean() + @IsOptional() + thinkingIndicatorEnabled?: boolean; +} diff --git a/apps/api/src/app/agents/dtos/agent-response.dto.ts b/apps/api/src/app/agents/dtos/agent-response.dto.ts index 685f0310ebc..adef54ac260 100644 --- a/apps/api/src/app/agents/dtos/agent-response.dto.ts +++ b/apps/api/src/app/agents/dtos/agent-response.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { AgentBehaviorDto } from './agent-behavior.dto'; import { AgentIntegrationSummaryDto } from './agent-integration-summary.dto'; export class AgentResponseDto { @@ -15,6 +16,9 @@ export class AgentResponseDto { @ApiPropertyOptional() description?: string; + @ApiPropertyOptional({ type: AgentBehaviorDto }) + behavior?: AgentBehaviorDto; + @ApiProperty() _environmentId: string; diff --git a/apps/api/src/app/agents/dtos/index.ts b/apps/api/src/app/agents/dtos/index.ts index 89ef735ab40..bbb0beb6cab 100644 --- a/apps/api/src/app/agents/dtos/index.ts +++ b/apps/api/src/app/agents/dtos/index.ts @@ -1,4 +1,5 @@ export * from './add-agent-integration-request.dto'; +export * from './agent-behavior.dto'; export * from './agent-integration-summary.dto'; export * from './agent-integration-response.dto'; export * from './agent-response.dto'; diff --git a/apps/api/src/app/agents/dtos/update-agent-request.dto.ts b/apps/api/src/app/agents/dtos/update-agent-request.dto.ts index b7115bf6d01..57fc37d6967 100644 --- a/apps/api/src/app/agents/dtos/update-agent-request.dto.ts +++ b/apps/api/src/app/agents/dtos/update-agent-request.dto.ts @@ -1,5 +1,8 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; +import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +import { AgentBehaviorDto } from './agent-behavior.dto'; export class UpdateAgentRequestDto { @ApiPropertyOptional() @@ -11,4 +14,10 @@ export class UpdateAgentRequestDto { @IsString() @IsOptional() description?: string; + + @ApiPropertyOptional({ type: AgentBehaviorDto }) + @ValidateNested() + @Type(() => AgentBehaviorDto) + @IsOptional() + behavior?: AgentBehaviorDto; } diff --git a/apps/api/src/app/agents/e2e/agents.e2e.ts b/apps/api/src/app/agents/e2e/agents.e2e.ts index 80cedae52d2..ecd03161018 100644 --- a/apps/api/src/app/agents/e2e/agents.e2e.ts +++ b/apps/api/src/app/agents/e2e/agents.e2e.ts @@ -65,6 +65,39 @@ describe('Agents API - /agents #novu-v2', () => { expect(afterDelete.status).to.equal(404); }); + it('should update and return agent behavior settings', async () => { + const identifier = `e2e-behavior-${Date.now()}`; + + const createRes = await session.testAgent.post('/v1/agents').send({ + name: 'Behavior Agent', + identifier, + }); + + expect(createRes.status).to.equal(201); + expect(createRes.body.data.behavior).to.equal(undefined); + + const patchRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ + behavior: { thinkingIndicatorEnabled: false }, + }); + + expect(patchRes.status).to.equal(200); + expect(patchRes.body.data.behavior).to.deep.equal({ thinkingIndicatorEnabled: false }); + + const getRes = await session.testAgent.get(`/v1/agents/${encodeURIComponent(identifier)}`); + + expect(getRes.status).to.equal(200); + expect(getRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(false); + + const reEnableRes = await session.testAgent.patch(`/v1/agents/${encodeURIComponent(identifier)}`).send({ + behavior: { thinkingIndicatorEnabled: true }, + }); + + expect(reEnableRes.status).to.equal(200); + expect(reEnableRes.body.data.behavior.thinkingIndicatorEnabled).to.equal(true); + + await session.testAgent.delete(`/v1/agents/${encodeURIComponent(identifier)}`); + }); + it('should return 422 when identifier is not a valid slug', async () => { const res = await session.testAgent.post('/v1/agents').send({ name: 'Invalid Slug Agent', diff --git a/apps/api/src/app/agents/mappers/agent-response.mapper.ts b/apps/api/src/app/agents/mappers/agent-response.mapper.ts index fab4b1a0d0b..1ab93bfff36 100644 --- a/apps/api/src/app/agents/mappers/agent-response.mapper.ts +++ b/apps/api/src/app/agents/mappers/agent-response.mapper.ts @@ -8,6 +8,7 @@ export function toAgentResponse(agent: AgentEntity): AgentResponseDto { name: agent.name, identifier: agent.identifier, description: agent.description, + behavior: agent.behavior, _environmentId: agent._environmentId, _organizationId: agent._organizationId, createdAt: agent.createdAt, diff --git a/apps/api/src/app/agents/services/agent-credential.service.ts b/apps/api/src/app/agents/services/agent-credential.service.ts index feb2c3ad7f3..25b03861aec 100644 --- a/apps/api/src/app/agents/services/agent-credential.service.ts +++ b/apps/api/src/app/agents/services/agent-credential.service.ts @@ -20,6 +20,11 @@ export interface ResolvedPlatformConfig { agentIdentifier: string; integrationIdentifier: string; integrationId: string; + thinkingIndicatorEnabled: boolean; +} + +function resolveThinkingIndicator(agent: { behavior?: { thinkingIndicatorEnabled?: boolean } }): boolean { + return agent.behavior?.thinkingIndicatorEnabled !== false; } @Injectable() @@ -100,6 +105,7 @@ export class AgentCredentialService { agentIdentifier: agent.identifier, integrationIdentifier, integrationId: integration._id, + thinkingIndicatorEnabled: resolveThinkingIndicator(agent), }; } } 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 5bbbb5a1a9f..3b773db3f8f 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 @@ -75,7 +75,9 @@ export class AgentInboundHandler { organizationId: config.organizationId, }); - await thread.startTyping(); + if (config.thinkingIndicatorEnabled) { + await thread.startTyping('Thinking...'); + } const serializedThread = thread.toJSON() as unknown as Record; await this.conversationService.updateChannelThread( diff --git a/apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts b/apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts index f7f54f64632..02bde97cd7d 100644 --- a/apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts +++ b/apps/api/src/app/agents/usecases/update-agent/update-agent.command.ts @@ -1,6 +1,8 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; +import { AgentBehaviorDto } from '../../dtos/agent-behavior.dto'; export class UpdateAgentCommand extends EnvironmentWithUserCommand { @IsString() @@ -14,4 +16,9 @@ export class UpdateAgentCommand extends EnvironmentWithUserCommand { @IsString() @IsOptional() description?: string; + + @ValidateNested() + @Type(() => AgentBehaviorDto) + @IsOptional() + behavior?: AgentBehaviorDto; } diff --git a/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts b/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts index e972b2e1ffb..0ed25e1a5a4 100644 --- a/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts +++ b/apps/api/src/app/agents/usecases/update-agent/update-agent.usecase.ts @@ -10,8 +10,8 @@ export class UpdateAgent { constructor(private readonly agentRepository: AgentRepository) {} async execute(command: UpdateAgentCommand): Promise { - if (command.name === undefined && command.description === undefined) { - throw new BadRequestException('At least one of name or description must be provided.'); + if (command.name === undefined && command.description === undefined && command.behavior === undefined) { + throw new BadRequestException('At least one of name, description, or behavior must be provided.'); } const existing = await this.agentRepository.findOne( @@ -27,7 +27,7 @@ export class UpdateAgent { throw new NotFoundException(`Agent with identifier "${command.identifier}" was not found.`); } - const $set: Record = {}; + const $set: Record = {}; if (command.name !== undefined) { $set.name = command.name; @@ -37,6 +37,12 @@ export class UpdateAgent { $set.description = command.description; } + if (command.behavior !== undefined) { + if (command.behavior.thinkingIndicatorEnabled !== undefined) { + $set['behavior.thinkingIndicatorEnabled'] = command.behavior.thinkingIndicatorEnabled; + } + } + await this.agentRepository.updateOne( { _id: existing._id, diff --git a/libs/dal/src/repositories/agent/agent.entity.ts b/libs/dal/src/repositories/agent/agent.entity.ts index c7e83c45f03..0b376755aed 100644 --- a/libs/dal/src/repositories/agent/agent.entity.ts +++ b/libs/dal/src/repositories/agent/agent.entity.ts @@ -2,6 +2,10 @@ import type { ChangePropsValueType } from '../../types/helpers'; import type { EnvironmentId } from '../environment'; import type { OrganizationId } from '../organization'; +export interface AgentBehavior { + thinkingIndicatorEnabled?: boolean; +} + export class AgentEntity { _id: string; @@ -11,6 +15,8 @@ export class AgentEntity { description?: string; + behavior?: AgentBehavior; + _environmentId: EnvironmentId; _organizationId: OrganizationId; diff --git a/libs/dal/src/repositories/agent/agent.schema.ts b/libs/dal/src/repositories/agent/agent.schema.ts index 482ce9b7121..908ebf37f94 100644 --- a/libs/dal/src/repositories/agent/agent.schema.ts +++ b/libs/dal/src/repositories/agent/agent.schema.ts @@ -14,6 +14,9 @@ const agentSchema = new Schema( required: true, }, description: Schema.Types.String, + behavior: { + thinkingIndicatorEnabled: Schema.Types.Boolean, + }, _organizationId: { type: Schema.Types.ObjectId, ref: 'Organization',