Migrate agent v2 (#16214)

# Introduction
Closes https://github.com/twentyhq/core-team-issues/issues/1980

In this PR we migrate the agent from v1 to v2.

## New FlatRoleTargetByAgentIdMaps
Derivated the `flatRoleTargetMaps` to be building a
`flatRoleTargetByAgentIdMaps` to ease retrieving a roleId to associate
to an agent

## Coverage
Added strong coverage on both failing and successful CRU agents
operations

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Paul Rastoin
2025-12-03 11:51:19 +01:00
committed by GitHub
parent a7c5969ede
commit 59672e3e34
110 changed files with 4864 additions and 849 deletions
@@ -9,4 +9,7 @@ export enum AgentExceptionCode {
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS = 'ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS',
INVALID_AGENT_INPUT = 'INVALID_AGENT_INPUT',
AGENT_ALREADY_EXISTS = 'AGENT_ALREADY_EXISTS',
AGENT_IS_STANDARD = 'AGENT_IS_STANDARD',
}
@@ -1,4 +1,4 @@
import { UseGuards } from '@nestjs/common';
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@@ -11,6 +11,8 @@ import {
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { AgentService } from './agent.service';
@@ -18,20 +20,30 @@ import { AgentIdInput } from './dtos/agent-id.input';
import { AgentDTO } from './dtos/agent.dto';
import { CreateAgentInput } from './dtos/create-agent.input';
import { UpdateAgentInput } from './dtos/update-agent.input';
import { AgentGraphqlApiExceptionInterceptor } from './interceptors/agent-graphql-api-exception.interceptor';
@UseGuards(
WorkspaceAuthGuard,
FeatureFlagGuard,
SettingsPermissionGuard(PermissionFlagType.AI),
)
@UseInterceptors(
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
AgentGraphqlApiExceptionInterceptor,
)
@Resolver()
export class AgentResolver {
constructor(private readonly agentService: AgentService) {}
@Query(() => [AgentDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async findManyAgents(@AuthWorkspace() { id: workspaceId }: WorkspaceEntity) {
return this.agentService.findManyAgents(workspaceId);
async findManyAgents(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<AgentDTO[]> {
const flatAgentsWithRoleId =
await this.agentService.findManyAgents(workspaceId);
return flatAgentsWithRoleId.map(fromFlatAgentWithRoleIdToAgentDto);
}
@Query(() => AgentDTO)
@@ -39,8 +51,13 @@ export class AgentResolver {
async findOneAgent(
@Args('input') { id }: AgentIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
return this.agentService.findOneAgent(workspaceId, { id });
): Promise<AgentDTO> {
const fatAgentWithRoleId = await this.agentService.findOneAgentById({
workspaceId,
id,
});
return fromFlatAgentWithRoleIdToAgentDto(fatAgentWithRoleId);
}
@Mutation(() => AgentDTO)
@@ -49,11 +66,13 @@ export class AgentResolver {
async createOneAgent(
@Args('input') input: CreateAgentInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
return this.agentService.createOneAgent(
): Promise<AgentDTO> {
const createdAgent = await this.agentService.createOneAgent(
{ ...input, isCustom: true },
workspaceId,
);
return fromFlatAgentWithRoleIdToAgentDto(createdAgent);
}
@Mutation(() => AgentDTO)
@@ -62,8 +81,13 @@ export class AgentResolver {
async updateOneAgent(
@Args('input') input: UpdateAgentInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
return this.agentService.updateOneAgent(input, workspaceId);
): Promise<AgentDTO> {
const updatedAgent = await this.agentService.updateOneAgent({
input,
workspaceId,
});
return fromFlatAgentWithRoleIdToAgentDto(updatedAgent);
}
@Mutation(() => AgentDTO)
@@ -72,7 +96,12 @@ export class AgentResolver {
async deleteOneAgent(
@Args('input') { id }: AgentIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
return this.agentService.deleteOneAgent(id, workspaceId);
): Promise<AgentDTO> {
const deletedFlatAgent = await this.agentService.deleteOneAgent(
id,
workspaceId,
);
return fromFlatAgentWithRoleIdToAgentDto(deletedFlatAgent);
}
}
@@ -1,14 +1,21 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { computeMetadataNameFromLabel } from 'twenty-shared/metadata';
import { In, Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type CreateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/create-agent.input';
import { type UpdateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/update-agent.input';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { fromCreateAgentInputToFlatAgent } from 'src/engine/metadata-modules/ai/ai-agent/utils/from-create-agent-input-to-flat-agent.util';
import { fromUpdateAgentInputToFlatAgentToUpdate } from 'src/engine/metadata-modules/ai/ai-agent/utils/from-update-agent-input-to-flat-agent-to-update.util';
import { FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
import { AgentException, AgentExceptionCode } from './agent.exception';
@@ -19,110 +26,43 @@ export class AgentService {
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
private readonly agentRoleService: AiAgentRoleService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
async findManyAgents(workspaceId: string) {
const agents = await this.agentRepository.find({
where: { workspaceId },
order: { createdAt: 'DESC' },
});
async findManyAgents(workspaceId: string): Promise<FlatAgentWithRoleId[]> {
const { flatAgentMaps, flatRoleTargetByAgentIdMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetByAgentIdMaps',
]);
if (agents.length === 0) {
return [];
}
return Object.values(flatAgentMaps.byId)
.filter(isDefined)
.map((flatAgent) => {
const roleId = flatRoleTargetByAgentIdMaps[flatAgent.id]?.roleId;
const agentRoleMap = await this.buildAgentRoleMap(workspaceId, agents);
return agents.map((agent) => ({
...agent,
roleId: agentRoleMap.get(agent.id) || null,
}));
return {
...flatAgent,
roleId: roleId ?? null,
};
});
}
private async buildAgentRoleMap(
workspaceId: string,
agents: AgentEntity[],
): Promise<Map<string, string>> {
const roleTargets = await this.roleTargetRepository.find({
where: {
workspaceId,
agentId: In(agents.map((agent) => agent.id)),
},
});
const agentRoleMap = new Map<string, string>();
roleTargets.forEach((roleTarget) => {
if (roleTarget.agentId) {
agentRoleMap.set(roleTarget.agentId, roleTarget.roleId);
}
});
return agentRoleMap;
}
async findOneByApplicationAndStandardId({
applicationId,
standardId,
async findOneAgentByName({
name,
workspaceId,
}: {
applicationId: string;
standardId: string;
workspaceId: string;
}) {
return await this.agentRepository.findOne({
where: { applicationId, standardId, workspaceId },
});
}
async findOneAgent(
workspaceId: string,
{ id, name }: { id?: string; name?: string },
) {
this.validateAgentIdentifier(id, name);
const agent = await this.fetchAgent(workspaceId, id, name);
const roleId = await this.fetchAgentRoleId(workspaceId, agent.id);
return {
...agent,
roleId,
};
}
private validateAgentIdentifier(
id: string | undefined,
name: string | undefined,
): void {
if (!isNonEmptyString(id) && !isNonEmptyString(name)) {
throw new AgentException(
'Either id or name must be provided',
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
if (isNonEmptyString(id) && isNonEmptyString(name)) {
throw new AgentException(
'Cannot specify both id and name',
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
}
private async fetchAgent(
workspaceId: string,
id: string | undefined,
name: string | undefined,
): Promise<AgentEntity> {
name: string;
}): Promise<AgentEntity> {
const agent = await this.agentRepository.findOne({
where: id ? { id, workspaceId } : { name, workspaceId },
where: { name, workspaceId },
});
if (!agent) {
const identifier = id ? `id "${id}"` : `name "${name}"`;
const identifier = `name "${name}"`;
throw new AgentException(
`Agent with ${identifier} not found`,
@@ -133,125 +73,267 @@ export class AgentService {
return agent;
}
private async fetchAgentRoleId(
workspaceId: string,
agentId: string,
): Promise<string | null> {
const roleTarget = await this.roleTargetRepository.findOne({
where: {
agentId,
workspaceId,
},
select: ['roleId'],
async findOneAgentById({
id,
workspaceId,
}: {
workspaceId: string;
id: string;
}): Promise<FlatAgentWithRoleId> {
const { flatAgentMaps, flatRoleTargetByAgentIdMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetByAgentIdMaps',
]);
const flatAgent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatAgentMaps,
});
return roleTarget?.roleId || null;
if (!isDefined(flatAgent)) {
throw new AgentException(
`Agent not found`,
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
const roleId = flatRoleTargetByAgentIdMaps[flatAgent.id]?.roleId;
return { ...flatAgent, roleId: roleId ?? null };
}
async createOneAgent(
input: CreateAgentInput & { isCustom: boolean },
workspaceId: string,
) {
const agent = this.buildNewAgent(input, workspaceId);
const createdAgent = await this.agentRepository.save(agent);
): Promise<FlatAgentWithRoleId> {
const {
flatAgentMaps: existingFlatAgentMaps,
flatRoleTargetMaps: existingFlatRoleTargetMaps,
flatRoleMaps: existingFlatRoleMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetMaps',
'flatRoleMaps',
]);
if (isNonEmptyString(input.roleId)) {
await this.assignRoleToNewAgent(
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatAgentToCreate, flatRoleTargetToCreate } =
fromCreateAgentInputToFlatAgent({
createAgentInput: {
...input,
applicationId:
input.applicationId ?? workspaceCustomFlatApplication.id,
},
workspaceId,
createdAgent.id,
input.roleId,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatAgentMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatAgentMaps,
flatEntityToCreate: [flatAgentToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
}),
flatRoleTargetMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatRoleTargetMaps,
flatEntityToCreate: isDefined(flatRoleTargetToCreate)
? [flatRoleTargetToCreate]
: [],
flatEntityToDelete: [],
flatEntityToUpdate: [],
}),
},
dependencyAllFlatEntityMaps: { flatRoleMaps: existingFlatRoleMaps },
buildOptions: {
isSystemBuild: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating agent',
);
}
return this.findOneAgent(workspaceId, { id: createdAgent.id });
}
const { flatAgentMaps: recomputedFlatAgentMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
]);
private buildNewAgent(
input: CreateAgentInput & { isCustom: boolean },
workspaceId: string,
): AgentEntity {
return this.agentRepository.create({
...input,
name: isNonEmptyString(input.name)
? input.name
: computeMetadataNameFromLabel(input.label),
workspaceId,
isCustom: input.isCustom,
const createdAgent = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatAgentToCreate.id,
flatEntityMaps: recomputedFlatAgentMaps,
});
}
private async assignRoleToNewAgent(
workspaceId: string,
agentId: string,
roleId: string,
): Promise<void> {
await this.agentRoleService.assignRoleToAgent({
workspaceId,
agentId,
roleId,
});
}
async updateOneAgent(input: UpdateAgentInput, workspaceId: string) {
const agent = await this.findOneAgent(workspaceId, { id: input.id });
const updateData = this.buildUpdateData(agent, input);
const updatedAgent = await this.agentRepository.save(updateData);
if (!('roleId' in input)) {
return updatedAgent;
}
await this.updateAgentRole(workspaceId, agent.id, input.roleId);
return this.findOneAgent(workspaceId, { id: updatedAgent.id });
}
private buildUpdateData(
agent: AgentEntity & { roleId: string | null },
input: UpdateAgentInput,
): Partial<AgentEntity> {
const updateData: Partial<AgentEntity> = {
...agent,
...Object.fromEntries(
Object.entries(input).filter(([_, value]) => value !== undefined),
),
return {
...createdAgent,
roleId: flatRoleTargetToCreate?.roleId ?? null,
};
if (input.label !== undefined) {
updateData.name = computeMetadataNameFromLabel(input.label);
} else if (input.name !== undefined) {
updateData.name = input.name;
}
return updateData;
}
private async updateAgentRole(
workspaceId: string,
agentId: string,
roleId: string | null | undefined,
): Promise<void> {
if (isNonEmptyString(roleId)) {
await this.agentRoleService.assignRoleToAgent({
workspaceId,
agentId,
roleId,
});
async updateOneAgent({
input,
workspaceId,
}: {
input: UpdateAgentInput;
workspaceId: string;
}): Promise<FlatAgentWithRoleId> {
const {
flatAgentMaps: existingFlatAgentMaps,
flatRoleTargetMaps: existingFlatRoleTargetMaps,
flatRoleMaps: existingFlatRoleMaps,
flatRoleTargetByAgentIdMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetMaps',
'flatRoleMaps',
'flatRoleTargetByAgentIdMaps',
]);
return;
}
await this.agentRoleService.removeRoleFromAgent({
workspaceId,
agentId,
const {
flatAgentToUpdate,
flatRoleTargetToCreate,
flatRoleTargetToDelete,
flatRoleTargetToUpdate,
} = fromUpdateAgentInputToFlatAgentToUpdate({
updateAgentInput: input,
flatAgentMaps: existingFlatAgentMaps,
flatRoleTargetByAgentIdMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatAgentMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatAgentMaps,
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatAgentToUpdate],
}),
flatRoleTargetMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatRoleTargetMaps,
flatEntityToCreate: isDefined(flatRoleTargetToCreate)
? [flatRoleTargetToCreate]
: [],
flatEntityToDelete: isDefined(flatRoleTargetToDelete)
? [flatRoleTargetToDelete]
: [],
flatEntityToUpdate: isDefined(flatRoleTargetToUpdate)
? [flatRoleTargetToUpdate]
: [],
}),
},
dependencyAllFlatEntityMaps: { flatRoleMaps: existingFlatRoleMaps },
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: {
roleTarget: isDefined(flatRoleTargetToDelete),
},
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating agent',
);
}
const {
flatAgentMaps: recomputedFlatAgentMaps,
flatRoleTargetByAgentIdMaps: recmputedFlatRoleTargetByAgentIdMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetByAgentIdMaps',
]);
const updatedAgent = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedFlatAgentMaps,
});
const existingRoleTarget =
recmputedFlatRoleTargetByAgentIdMaps[flatAgentToUpdate.id];
return {
...updatedAgent,
roleId: existingRoleTarget?.roleId ?? null,
};
}
async deleteOneAgent(id: string, workspaceId: string) {
const agent = await this.findOneAgent(workspaceId, { id });
async deleteOneAgent(
id: string,
workspaceId: string,
): Promise<FlatAgentWithRoleId> {
const {
flatAgentMaps: existingFlatAgentMaps,
flatRoleTargetByAgentIdMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetByAgentIdMaps',
]);
await this.agentRepository.softDelete({ id: agent.id });
const agentToDelete = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: existingFlatAgentMaps,
});
return agent;
if (!isDefined(agentToDelete)) {
throw new AgentException(
`Agent not found`,
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
const roleId = flatRoleTargetByAgentIdMaps[agentToDelete.id]?.roleId;
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatAgentMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatAgentMaps,
flatEntityToCreate: [],
flatEntityToDelete: [agentToDelete],
flatEntityToUpdate: [],
}),
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: {
agent: true,
},
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting agent',
);
}
return {
...agentToDelete,
roleId: roleId ?? null,
};
}
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
@@ -10,18 +11,23 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AgentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { AiChatRouterModule } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { AiToolsModule } from 'src/engine/metadata-modules/ai/ai-tools/ai-tools.module';
import { FlatAgentModule } from 'src/engine/metadata-modules/flat-agent/flat-agent.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { AgentResolver } from './agent.resolver';
import { AgentService } from './agent.service';
@@ -55,6 +61,11 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi
UserWorkspaceModule,
UserRoleModule,
WorkspaceCacheModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
ApplicationModule,
FlatAgentModule,
WorkspaceCacheModule,
],
providers: [
AgentResolver,
@@ -65,6 +76,8 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi
AgentToolGeneratorService,
AgentTitleGenerationService,
AgentActorContextService,
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
AgentGraphqlApiExceptionInterceptor,
],
exports: [
AgentService,
@@ -21,7 +21,7 @@ export class AgentDTO {
id: string;
@Field(() => UUIDScalarType, { nullable: true })
standardId: string | null;
standardId?: string | null;
@IsString()
@Field()
@@ -37,7 +37,7 @@ export class AgentDTO {
@IsString()
@Field({ nullable: true })
description: string;
description?: string;
@IsString()
@IsNotEmpty()
@@ -48,7 +48,7 @@ export class AgentDTO {
modelId: ModelId;
@Field(() => GraphQLJSON, { nullable: true })
responseFormat: object;
responseFormat?: object;
@Field(() => UUIDScalarType, { nullable: true })
roleId?: string;
@@ -72,7 +72,7 @@ export class AgentDTO {
updatedAt: Date;
@Field(() => GraphQLJSON, { nullable: true })
modelConfiguration: ModelConfiguration;
modelConfiguration?: ModelConfiguration;
@Field(() => [String])
evaluationInputs: string[];
@@ -1,5 +1,6 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsArray,
IsNotEmpty,
@@ -7,12 +8,16 @@ import {
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
import { ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { AgentResponseFormatJson } from 'src/engine/metadata-modules/ai/ai-agent/validators/agent-response-format-json.validator';
import { AgentResponseFormatText } from 'src/engine/metadata-modules/ai/ai-agent/validators/agent-response-format-text.validator';
@InputType()
export class CreateAgentInput {
@@ -51,10 +56,20 @@ export class CreateAgentInput {
@Field(() => UUIDScalarType, { nullable: true })
roleId?: string;
@IsObject()
@IsOptional()
@ValidateNested()
@Type(() => Object, {
keepDiscriminatorProperty: true,
discriminator: {
property: 'type',
subTypes: [
{ value: AgentResponseFormatText, name: 'text' },
{ value: AgentResponseFormatJson, name: 'json' },
],
},
})
@Field(() => GraphQLJSON, { nullable: true })
responseFormat?: object;
responseFormat?: AgentResponseFormat;
@IsObject()
@IsOptional()
@@ -1,5 +1,6 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsArray,
IsNotEmpty,
@@ -7,11 +8,15 @@ import {
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
import { AgentResponseFormatJson } from 'src/engine/metadata-modules/ai/ai-agent/validators/agent-response-format-json.validator';
import { AgentResponseFormatText } from 'src/engine/metadata-modules/ai/ai-agent/validators/agent-response-format-text.validator';
import { ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
@InputType()
@@ -54,12 +59,22 @@ export class UpdateAgentInput {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
roleId?: string;
roleId?: string | null;
@IsObject()
@IsOptional()
@ValidateNested()
@Type(() => Object, {
keepDiscriminatorProperty: true,
discriminator: {
property: 'type',
subTypes: [
{ value: AgentResponseFormatText, name: 'text' },
{ value: AgentResponseFormatJson, name: 'json' },
],
},
})
@Field(() => GraphQLJSON, { nullable: true })
responseFormat?: object;
responseFormat?: AgentResponseFormat;
@IsObject()
@IsOptional()
@@ -43,11 +43,11 @@ export class AgentEntity
@Column({ nullable: false })
label: string;
@Column({ nullable: true })
icon: string;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({ nullable: true })
description: string;
@Column({ nullable: true, type: 'text' })
description: string | null;
@Column({ nullable: false, type: 'text' })
prompt: string;
@@ -55,6 +55,7 @@ export class AgentEntity
@Column({ nullable: false, type: 'varchar', default: DEFAULT_SMART_MODEL })
modelId: ModelId;
// Should not be nullable
@Column({ nullable: true, type: 'jsonb', default: { type: 'text' } })
responseFormat: AgentResponseFormat;
@@ -80,7 +81,7 @@ export class AgentEntity
deletedAt: Date | null;
@Column({ nullable: true, type: 'jsonb' })
modelConfiguration: ModelConfiguration;
modelConfiguration: ModelConfiguration | null;
@Column({ type: 'text', array: true, default: '{}' })
evaluationInputs: string[];
@@ -0,0 +1,20 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { type Observable, catchError } from 'rxjs';
import { agentGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/ai/ai-agent/utils/agent-graphql-api-exception-handler.util';
@Injectable()
export class AgentGraphqlApiExceptionInterceptor implements NestInterceptor {
intercept(
_context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next.handle().pipe(catchError(agentGraphqlApiExceptionHandler));
}
}
@@ -24,7 +24,6 @@ import {
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AgentActorContextService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-actor-context.service';
import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-model-config.service';
import { AgentToolGeneratorService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-tool-generator.service';
@@ -34,6 +33,7 @@ import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/serv
import { ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -77,7 +77,7 @@ export class AgentExecutionService {
toolHints,
}: {
system: string;
agent: AgentEntity | null;
agent: FlatAgentWithRoleId | null;
messages: UIMessage<unknown, UIDataTypes, UITools>[];
actorContext?: ActorMetadata;
roleIds?: string[];
@@ -300,7 +300,8 @@ export class AgentExecutionService {
};
}> {
try {
const agent = await this.agentService.findOneAgent(workspace.id, {
const agent = await this.agentService.findOneAgentById({
workspaceId: workspace.id,
id: agentId,
});
@@ -6,9 +6,9 @@ import { ProviderOptions } from '@ai-sdk/provider-utils';
import { ToolSet } from 'ai';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { RegisteredAIModel } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
@Injectable()
export class AgentModelConfigService {
@@ -16,7 +16,7 @@ export class AgentModelConfigService {
getProviderOptions(
model: RegisteredAIModel,
agent: AgentEntity,
agent: FlatAgentWithRoleId,
): ProviderOptions {
switch (model.provider) {
case ModelProvider.XAI:
@@ -28,7 +28,10 @@ export class AgentModelConfigService {
}
}
getNativeModelTools(model: RegisteredAIModel, agent: AgentEntity): ToolSet {
getNativeModelTools(
model: RegisteredAIModel,
agent: FlatAgentWithRoleId,
): ToolSet {
const tools: ToolSet = {};
if (!agent.modelConfiguration) {
@@ -51,7 +54,7 @@ export class AgentModelConfigService {
return tools;
}
private getXaiProviderOptions(agent: AgentEntity): ProviderOptions {
private getXaiProviderOptions(agent: FlatAgentWithRoleId): ProviderOptions {
if (
!agent.modelConfiguration ||
(!agent.modelConfiguration.webSearch?.enabled &&
@@ -71,10 +71,10 @@ export class AgentPlanExecutorService {
);
const agent =
await this.agentExecutionService.agentService.findOneAgent(
workspace.id,
{ name: step.agentName },
);
await this.agentExecutionService.agentService.findOneAgentByName({
name: step.agentName,
workspaceId: workspace.id,
});
this.logger.log(
`[PLAN EXECUTION] Step ${step.stepNumber}: Found agent "${agent.label}" (${agent.id})`,
@@ -1,10 +1,12 @@
import { type AgentResponseSchema } from 'twenty-shared/ai';
export type AgentResponseFormatType = 'text' | 'json';
export type AgentResponseFormatType = AgentResponseFormat['type'];
export type AgentTextResponseFormat = { type: 'text' };
export type AgentJsonResponseFormat = {
type: 'json';
schema: AgentResponseSchema;
};
export type AgentResponseFormat =
| { type: 'text' }
| {
type: 'json';
schema: AgentResponseSchema;
};
| AgentTextResponseFormat
| AgentJsonResponseFormat;
@@ -0,0 +1,38 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
ConflictError,
ForbiddenError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
export const agentGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof AgentException) {
switch (error.code) {
case AgentExceptionCode.AGENT_NOT_FOUND:
case AgentExceptionCode.ROLE_NOT_FOUND:
throw new NotFoundError(error);
case AgentExceptionCode.INVALID_AGENT_INPUT:
throw new UserInputError(error);
case AgentExceptionCode.AGENT_ALREADY_EXISTS:
throw new ConflictError(error);
case AgentExceptionCode.AGENT_IS_STANDARD:
case AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
throw new ForbiddenError(error);
case AgentExceptionCode.AGENT_EXECUTION_FAILED:
case AgentExceptionCode.API_KEY_NOT_CONFIGURED:
case AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
throw error;
default: {
return assertUnreachable(error.code);
}
}
}
throw error;
};
@@ -0,0 +1,88 @@
import { isNonEmptyString } from '@sniptt/guards';
import {
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { computeMetadataNameFromLabel } from 'twenty-shared/metadata';
import { type CreateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/create-agent.input';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export type FromCreateAgentInputToFlatAgentArgs = {
createAgentInput: CreateAgentInput & { applicationId: string };
workspaceId: string;
};
export const fromCreateAgentInputToFlatAgent = ({
createAgentInput: rawCreateAgentInput,
workspaceId,
}: FromCreateAgentInputToFlatAgentArgs): {
flatAgentToCreate: FlatAgent;
flatRoleTargetToCreate: FlatRoleTarget | null;
} => {
const { roleId, ...createAgentInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateAgentInput,
[
'name',
'label',
'icon',
'description',
'prompt',
'modelId',
'standardId',
'applicationId',
'roleId',
],
);
const createdAt = new Date();
const agentId = v4();
const standardId = createAgentInput.standardId ?? null;
const universalIdentifier = standardId ?? agentId;
const flatAgentToCreate: FlatAgent = {
id: agentId,
standardId,
name: isNonEmptyString(createAgentInput.name)
? createAgentInput.name
: computeMetadataNameFromLabel(createAgentInput.label),
label: createAgentInput.label,
icon: createAgentInput.icon ?? null,
description: createAgentInput.description ?? null,
prompt: createAgentInput.prompt,
modelId: createAgentInput.modelId,
responseFormat: createAgentInput.responseFormat ?? { type: 'text' },
workspaceId,
isCustom: true,
universalIdentifier,
applicationId: createAgentInput.applicationId,
modelConfiguration: createAgentInput.modelConfiguration ?? null,
evaluationInputs: createAgentInput.evaluationInputs ?? [],
createdAt,
updatedAt: createdAt,
deletedAt: null,
};
const flatRoleTargetToCreate: FlatRoleTarget | null = isDefined(roleId)
? {
id: v4(),
roleId,
userWorkspaceId: null,
agentId,
apiKeyId: null,
createdAt,
updatedAt: createdAt,
universalIdentifier: v4(),
workspaceId,
applicationId: createAgentInput.applicationId,
}
: null;
return {
flatAgentToCreate,
flatRoleTargetToCreate,
};
};
@@ -0,0 +1,146 @@
import { msg } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { type UpdateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/update-agent.input';
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant';
import { type FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
type FlatRoleTargetToUpdateCreateDelete = {
flatRoleTargetToUpdate?: FlatRoleTarget;
flatRoleTargetToCreate?: FlatRoleTarget;
flatRoleTargetToDelete?: FlatRoleTarget;
};
const computeAgentFlatRoleTargetToUpdate = ({
roleId,
flatAgent,
flatRoleTargetByAgentIdMaps,
}: {
roleId: string | null | undefined;
flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps;
flatAgent: FlatAgent;
}): FlatRoleTargetToUpdateCreateDelete => {
if (roleId === undefined) {
return {};
}
const existingRoleTarget = flatRoleTargetByAgentIdMaps[flatAgent.id];
const updatedAt = new Date();
if (roleId === null) {
if (isDefined(existingRoleTarget)) {
return {
flatRoleTargetToDelete: existingRoleTarget,
};
}
return {};
}
if (isDefined(existingRoleTarget)) {
return {
flatRoleTargetToUpdate: {
...existingRoleTarget,
roleId,
updatedAt,
},
};
}
return {
flatRoleTargetToCreate: {
id: v4(),
roleId,
userWorkspaceId: null,
agentId: flatAgent.id,
apiKeyId: null,
createdAt: updatedAt,
updatedAt,
universalIdentifier: v4(),
workspaceId: flatAgent.workspaceId,
applicationId: flatAgent.applicationId,
},
};
};
export type FromUpdateAgentInputToFlatAgentToUpdateArgs = {
updateAgentInput: UpdateAgentInput;
flatAgentMaps: FlatAgentMaps;
flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps;
};
export const fromUpdateAgentInputToFlatAgentToUpdate = ({
updateAgentInput: rawUpdateAgentInput,
flatAgentMaps,
flatRoleTargetByAgentIdMaps,
}: FromUpdateAgentInputToFlatAgentToUpdateArgs): {
flatAgentToUpdate: FlatAgent;
} & FlatRoleTargetToUpdateCreateDelete => {
const { id: agentIdToUpdate } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateAgentInput,
['id'],
);
const existingFlatAgent = flatAgentMaps.byId[agentIdToUpdate];
if (!isDefined(existingFlatAgent)) {
throw new AgentException(
'Agent not found',
AgentExceptionCode.AGENT_NOT_FOUND,
{
userFriendlyMessage: msg`The agent you are looking for could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
const updatedEditableAgentProperties = extractAndSanitizeObjectStringFields(
rawUpdateAgentInput,
FLAT_AGENT_EDITABLE_PROPERTIES,
);
if (
isDefined(updatedEditableAgentProperties.label) &&
!isDefined(updatedEditableAgentProperties.name)
) {
updatedEditableAgentProperties.name = computeMetadataNameFromLabelOrThrow(
updatedEditableAgentProperties.label,
);
}
const flatAgentToUpdate: FlatAgent = mergeUpdateInExistingRecord({
existing: existingFlatAgent,
properties: FLAT_AGENT_EDITABLE_PROPERTIES,
update: updatedEditableAgentProperties,
});
const {
flatRoleTargetToUpdate,
flatRoleTargetToCreate,
flatRoleTargetToDelete,
} = computeAgentFlatRoleTargetToUpdate({
roleId: rawUpdateAgentInput.roleId,
flatAgent: existingFlatAgent,
flatRoleTargetByAgentIdMaps,
});
return {
flatAgentToUpdate,
flatRoleTargetToUpdate,
flatRoleTargetToCreate,
flatRoleTargetToDelete,
};
};
@@ -0,0 +1,10 @@
import { IsEnum, IsNotEmpty, IsObject } from 'class-validator';
export class AgentResponseFormatJson {
@IsEnum(['json'])
type: 'json';
@IsObject()
@IsNotEmpty()
schema: Record<string, unknown>;
}
@@ -0,0 +1,6 @@
import { IsEnum } from 'class-validator';
export class AgentResponseFormatText {
@IsEnum(['text'])
type: 'text';
}
@@ -0,0 +1,13 @@
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
export const FLAT_AGENT_EDITABLE_PROPERTIES = [
'name',
'label',
'icon',
'description',
'prompt',
'modelId',
'responseFormat',
'modelConfiguration',
'evaluationInputs',
] as const satisfies (keyof FlatAgent)[];
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { WorkspaceFlatAgentMapCacheService } from 'src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service';
import { WorkspaceFlatRoleTargetByAgentIdService } from 'src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
@Module({
imports: [
TypeOrmModule.forFeature([AgentEntity, RoleTargetEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [
WorkspaceFlatAgentMapCacheService,
WorkspaceFlatRoleTargetByAgentIdService,
],
exports: [
WorkspaceFlatAgentMapCacheService,
WorkspaceFlatRoleTargetByAgentIdService,
],
})
export class FlatAgentModule {}
@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type';
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatAgentMaps')
export class WorkspaceFlatAgentMapCacheService extends WorkspaceCacheProvider<FlatAgentMaps> {
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatAgentMaps> {
const agents = await this.agentRepository.find({
where: { workspaceId },
withDeleted: true,
});
const flatAgentMaps = createEmptyFlatEntityMaps();
for (const agentEntity of agents) {
const flatAgent = transformAgentEntityToFlatAgent(agentEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatAgent,
flatEntityMapsToMutate: flatAgentMaps,
});
}
return flatAgentMaps;
}
}
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { NonNullableRequired } from 'twenty-shared/types';
import { IsNull, Not, Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
@Injectable()
@WorkspaceCache('flatRoleTargetByAgentIdMaps')
export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvider<FlatRoleTargetByAgentIdMaps> {
constructor(
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatRoleTargetByAgentIdMaps> {
const roleTargetEntities = await this.roleTargetRepository.find({
where: {
workspaceId,
agentId: Not(IsNull()),
},
withDeleted: true,
});
const flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps = {};
for (const roleTargetEntity of roleTargetEntities as Array<
Omit<RoleTargetEntity, 'agentId'> &
NonNullableRequired<Pick<RoleTargetEntity, 'agentId'>>
>) {
const flatRoleTarget =
fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity);
flatRoleTargetByAgentIdMaps[roleTargetEntity.agentId] = flatRoleTarget;
}
return flatRoleTargetByAgentIdMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
export type FlatAgentMaps = FlatEntityMaps<FlatAgent>;
@@ -11,5 +11,7 @@ export type AgentEntityRelationProperties =
export type FlatAgent = FlatEntityFrom<
AgentEntity,
AgentEntityRelationProperties | 'createdAt' | 'updatedAt' | 'deletedAt'
AgentEntityRelationProperties
>;
export type FlatAgentWithRoleId = FlatAgent & { roleId: string | null };
@@ -0,0 +1,5 @@
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export type FlatRoleTargetByAgentIdMaps = Partial<
Record<string, FlatRoleTarget>
>;
@@ -0,0 +1,40 @@
import { type AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
export const fromFlatAgentWithRoleIdToAgentDto = ({
applicationId,
createdAt,
description,
evaluationInputs,
icon,
id,
isCustom,
label,
modelConfiguration,
modelId,
name,
prompt,
responseFormat,
standardId,
updatedAt,
workspaceId,
roleId,
}: FlatAgentWithRoleId): AgentDTO => ({
createdAt,
description: description ?? undefined,
evaluationInputs,
id,
isCustom,
label,
modelConfiguration: modelConfiguration ?? undefined,
modelId,
name,
prompt,
responseFormat,
standardId,
updatedAt,
workspaceId,
applicationId: applicationId ?? undefined,
icon: icon ?? undefined,
roleId: roleId ?? undefined,
});
@@ -5,6 +5,9 @@ export const transformAgentEntityToFlatAgent = (
agentEntity: AgentEntity,
): FlatAgent => {
return {
createdAt: agentEntity.createdAt,
deletedAt: agentEntity.deletedAt,
updatedAt: agentEntity.updatedAt,
id: agentEntity.id,
standardId: agentEntity.standardId,
name: agentEntity.name,
@@ -1,22 +1,35 @@
import { v4 } from 'uuid';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
export const transformStandardAgentDefinitionToFlatAgent = (
standardAgentDefinition: StandardAgentDefinition,
workspaceId: string,
): FlatAgent => {
export const transformStandardAgentDefinitionToFlatAgent = ({
standardAgentDefinition,
workspaceId,
existingAgentEntity,
}: {
standardAgentDefinition: StandardAgentDefinition;
workspaceId: string;
existingAgentEntity?: Pick<
AgentEntity,
'createdAt' | 'deletedAt' | 'updatedAt' | 'id'
>;
}): FlatAgent => {
const {
standardRoleId: _standardRoleId,
outputStrategy: _outputStrategy,
...agentData
} = standardAgentDefinition;
const createdAt = new Date();
return {
id: existingAgentEntity?.id ?? v4(),
createdAt: existingAgentEntity?.createdAt ?? createdAt,
updatedAt: existingAgentEntity?.updatedAt ?? createdAt,
deletedAt: existingAgentEntity?.deletedAt ?? null,
...agentData,
id: v4(),
workspaceId,
universalIdentifier: standardAgentDefinition.standardId || v4(),
universalIdentifier: standardAgentDefinition.standardId,
};
};
@@ -2,6 +2,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
import { FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/cron-trigger/constants/flat-cron-trigger-editable-properties.constant';
import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-editable-properties.constant';
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant';
@@ -97,6 +98,14 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
propertiesToCompare: [...FLAT_ROLE_TARGET_EDITABLE_PROPERTIES],
propertiesToStringify: [],
},
agent: {
propertiesToCompare: [...FLAT_AGENT_EDITABLE_PROPERTIES],
propertiesToStringify: [
'responseFormat',
'modelConfiguration',
'evaluationInputs',
],
},
} as const satisfies {
[P in AllMetadataName]: OneFlatEntityConfiguration<P>;
};
@@ -112,4 +112,5 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
flatEntityForeignKeyAggregator: 'roleTargetIds',
},
},
agent: {},
} as const satisfies MetadataNameAndRelations;
@@ -1,15 +0,0 @@
export const ALL_METADATA_NAME = {
fieldMetadata: 'fieldMetadata',
objectMetadata: 'objectMetadata',
view: 'view',
viewField: 'viewField',
viewGroup: 'viewGroup',
index: 'index',
serverlessFunction: 'serverlessFunction',
cronTrigger: 'cronTrigger',
databaseEventTrigger: 'databaseEventTrigger',
routeTrigger: 'routeTrigger',
viewFilter: 'viewFilter',
role: 'role',
roleTarget: 'roleTarget',
} as const;
@@ -53,4 +53,5 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
roleTarget: {
role: true,
},
agent: {},
} as const satisfies MetadataRequiredForValidation;
@@ -1,12 +1,57 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service';
import { WorkspaceFlatIndexMapCacheService } from 'src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service';
import { WorkspaceFlatObjectMetadataMapCacheService } from 'src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service';
import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service';
import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service';
import { WorkspaceFlatViewGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service';
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceFlatMapCacheModule } from 'src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module';
@Module({
imports: [WorkspaceFlatMapCacheModule, WorkspaceCacheModule],
providers: [WorkspaceManyOrAllFlatEntityMapsCacheService],
exports: [WorkspaceManyOrAllFlatEntityMapsCacheService],
imports: [
WorkspaceCacheModule,
TypeOrmModule.forFeature([
ViewEntity,
ViewFieldEntity,
ViewFilterEntity,
ViewGroupEntity,
IndexMetadataEntity,
IndexFieldMetadataEntity,
FieldMetadataEntity,
ObjectMetadataEntity,
]),
],
providers: [
WorkspaceManyOrAllFlatEntityMapsCacheService,
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
],
exports: [
WorkspaceManyOrAllFlatEntityMapsCacheService,
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
],
})
export class WorkspaceManyOrAllFlatEntityMapsCacheModule {}
@@ -1,8 +1,10 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -24,6 +26,11 @@ import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/ent
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import {
type CreateAgentAction,
type DeleteAgentAction,
type UpdateAgentAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/types/workspace-migration-v2-agent-action-builder.service';
import {
type CreateCronTriggerAction,
type DeleteCronTriggerAction,
@@ -207,4 +214,13 @@ export type AllFlatEntityTypesByMetadataName = {
flatEntity: FlatRoleTarget;
entity: RoleTargetEntity;
};
agent: {
actions: {
created: CreateAgentAction;
updated: UpdateAgentAction;
deleted: DeleteAgentAction;
};
flatEntity: FlatAgent;
entity: AgentEntity;
};
};
@@ -1,3 +0,0 @@
import { type ALL_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant';
export type AllMetadataName = keyof typeof ALL_METADATA_NAME;
@@ -15,7 +15,7 @@ import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entiti
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -14,7 +14,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -13,7 +13,7 @@ import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entiti
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -7,6 +7,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { FlatAgentModule } from 'src/engine/metadata-modules/flat-agent/flat-agent.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatRoleTargetMapCacheService } from 'src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -51,6 +52,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
FileModule,
ApplicationModule,
WorkspaceCacheModule,
FlatAgentModule,
],
providers: [
RoleService,
@@ -32,6 +32,7 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
@@ -284,10 +285,13 @@ export class RoleResolver {
workspace.id,
);
return agents.map((agent) => ({
...agent,
applicationId: agent.applicationId ?? undefined,
}));
return agents.map((agentEntity) =>
fromFlatAgentWithRoleIdToAgentDto({
...agentEntity,
universalIdentifier: agentEntity.universalIdentifier ?? agentEntity.id,
roleId: role.id,
}),
);
}
@ResolveField('apiKeys', () => [ApiKeyForRoleDTO])
@@ -15,7 +15,7 @@ import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-fla
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -14,7 +14,7 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()