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
@@ -32,7 +32,6 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceFlatMapCacheModule } from 'src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module';
const restApiCoreResolvers = [
RestApiCreateOneHandler,
@@ -63,7 +62,6 @@ const restApiCoreResolvers = [
TwentyORMModule,
RecordTransformerModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceFlatMapCacheModule,
ActorModule,
FeatureFlagModule,
CoreCommonApiModule,
@@ -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()
@@ -2,6 +2,7 @@ import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/role-target/services/workspace-user-workspace-role-map-cache.service';
@@ -24,6 +25,8 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
flatApplicationMaps: 'flat-maps:flatApplicationMaps',
flatRoleMaps: 'flat-maps:role',
flatRoleTargetMaps: 'flat-maps:role-target',
flatAgentMaps: 'flat-maps:agent',
flatRoleTargetByAgentIdMaps: 'flat-maps:flatRoleTargetByAgentId',
} as const satisfies Record<WorkspaceCacheKeyName, string>;
type AdditionalCacheDataMap = {
@@ -32,6 +35,7 @@ type AdditionalCacheDataMap = {
userWorkspaceRoleMap: UserWorkspaceRoleMap;
apiKeyRoleMap: Record<string, string>;
flatApplicationMaps: FlatApplicationCacheMaps;
flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps;
};
export type WorkspaceCacheDataMap = AllFlatEntityMaps & AdditionalCacheDataMap;
@@ -1,11 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key';
export const WORKSPACE_FLAT_MAP_CACHE_KEY = 'workspaceFlatMapCacheKey';
export const WorkspaceFlatMapCache = (
cacheKey: MetadataToFlatEntityMapsKey<AllMetadataName>,
) => SetMetadata(WORKSPACE_FLAT_MAP_CACHE_KEY, cacheKey);
@@ -1,12 +0,0 @@
import {
appendCommonExceptionCode,
CustomException,
} from 'src/utils/custom-exception';
export class WorkspaceFlatMapCacheException extends CustomException<
keyof typeof WorkspaceFlatMapCacheExceptionCode
> {}
export const WorkspaceFlatMapCacheExceptionCode = appendCommonExceptionCode({
MISSING_DECORATOR: 'MISSING_DECORATOR',
} as const);
@@ -1,65 +0,0 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { isDefined } from 'twenty-shared/utils';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { WORKSPACE_FLAT_MAP_CACHE_KEY } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import {
WorkspaceFlatMapCacheException,
WorkspaceFlatMapCacheExceptionCode,
} from 'src/engine/workspace-flat-map-cache/exceptions/workspace-flat-map-cache.exception';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
export class WorkspaceFlatMapCacheRegistryService implements OnModuleInit {
private readonly cacheServiceRecord: {
[P in keyof AllFlatEntityMaps]?: WorkspaceFlatMapCacheService<
AllFlatEntityMaps[P]
>;
} = {};
constructor(private readonly discoveryService: DiscoveryService) {}
async onModuleInit() {
this.discoverAndRegisterCacheServices();
}
private discoverAndRegisterCacheServices(): void {
const providers = this.discoveryService.getProviders();
providers.forEach((wrapper) => {
const { instance, metatype } = wrapper;
if (!instance || !metatype) {
return;
}
const cacheKey: keyof AllFlatEntityMaps | undefined = Reflect.getMetadata(
WORKSPACE_FLAT_MAP_CACHE_KEY,
metatype,
);
if (cacheKey && instance instanceof WorkspaceFlatMapCacheService) {
this.cacheServiceRecord[cacheKey] = instance;
}
});
}
getCacheService<K extends keyof AllFlatEntityMaps>(flatMapsKey: K) {
return this.cacheServiceRecord[flatMapsKey];
}
getCacheServiceOrThrow<K extends keyof AllFlatEntityMaps>(flatMapsKey: K) {
const service = this.getCacheService(flatMapsKey);
if (!isDefined(service)) {
throw new WorkspaceFlatMapCacheException(
`No cache service found for ${flatMapsKey}`,
WorkspaceFlatMapCacheExceptionCode.INTERNAL_SERVER_ERROR,
);
}
return service;
}
}
@@ -1,189 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import crypto from 'crypto';
import { isDefined } from 'twenty-shared/utils';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { WORKSPACE_FLAT_MAP_CACHE_KEY } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import {
WorkspaceFlatMapCacheException,
WorkspaceFlatMapCacheExceptionCode,
} from 'src/engine/workspace-flat-map-cache/exceptions/workspace-flat-map-cache.exception';
@Injectable()
export abstract class WorkspaceFlatMapCacheService<T> {
protected readonly logger = new Logger(this.constructor.name);
private readonly localCacheFlatMaps = new Map<string, T>();
private readonly localCacheHashes = new Map<string, string>();
private readonly reflector = new Reflector();
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
private readonly cacheStorageService: CacheStorageService,
) {}
protected abstract computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<T>;
async getExistingOrRecomputeFlatMaps({
workspaceId,
}: {
workspaceId: string;
}): Promise<T> {
const localCacheHash = this.localCacheHashes.get(workspaceId);
const remoteCacheHash = await this.getHashFromRemoteCache({ workspaceId });
if (
isDefined(localCacheHash) &&
isDefined(remoteCacheHash) &&
localCacheHash === remoteCacheHash
) {
const localCacheFlatMap = this.localCacheFlatMaps.get(workspaceId);
if (localCacheFlatMap) {
return localCacheFlatMap;
}
}
if (remoteCacheHash) {
const remoteCacheFlatMap = await this.getFlatMapFromRemoteCache({
workspaceId,
});
if (remoteCacheFlatMap) {
this.localCacheFlatMaps.set(workspaceId, remoteCacheFlatMap);
this.localCacheHashes.set(workspaceId, remoteCacheHash);
return remoteCacheFlatMap;
}
}
const freshFlatMap = await this.recomputeAndStoreInCache({
workspaceId,
});
return freshFlatMap;
}
async recomputeAndStoreInCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<T> {
const freshFlatMap = await this.computeFlatMap({ workspaceId });
const newHash = this.generateHash({ flatMap: freshFlatMap });
await this.setFlatMapInRemoteCache({ workspaceId, flatMap: freshFlatMap });
await this.setHashInRemoteCache({ workspaceId, hash: newHash });
this.localCacheFlatMaps.set(workspaceId, freshFlatMap);
this.localCacheHashes.set(workspaceId, newHash);
return freshFlatMap;
}
async invalidateCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<void> {
await this.flushCache({
workspaceId,
});
await this.recomputeAndStoreInCache({ workspaceId });
}
private getFlatMapCacheKey(): string {
const cacheKey = this.reflector.get<string>(
WORKSPACE_FLAT_MAP_CACHE_KEY,
this.constructor,
);
if (!isDefined(cacheKey)) {
throw new WorkspaceFlatMapCacheException(
`${this.constructor.name} must be decorated with @WorkspaceFlatMapCache('cacheKey')`,
WorkspaceFlatMapCacheExceptionCode.MISSING_DECORATOR,
);
}
return cacheKey;
}
private buildRemoteCacheKeys({ workspaceId }: { workspaceId: string }) {
const cacheKey = this.getFlatMapCacheKey();
return {
flatMapKey: `flat-maps:${cacheKey}:${workspaceId}:flat-map`,
hashKey: `flat-maps:${cacheKey}:${workspaceId}:hash`,
};
}
private generateHash({ flatMap }: { flatMap: T }): string {
return crypto
.createHash('sha256')
.update(JSON.stringify(flatMap))
.digest('hex');
}
private async getHashFromRemoteCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<string | undefined> {
const { hashKey } = this.buildRemoteCacheKeys({ workspaceId });
return this.cacheStorageService.get<string>(hashKey);
}
private async setHashInRemoteCache({
workspaceId,
hash,
}: {
workspaceId: string;
hash: string;
}): Promise<void> {
const { hashKey } = this.buildRemoteCacheKeys({ workspaceId });
await this.cacheStorageService.set(hashKey, hash);
}
private async getFlatMapFromRemoteCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<T | undefined> {
const { flatMapKey } = this.buildRemoteCacheKeys({ workspaceId });
return this.cacheStorageService.get<T>(flatMapKey);
}
private async setFlatMapInRemoteCache({
workspaceId,
flatMap,
}: {
workspaceId: string;
flatMap: T;
}): Promise<void> {
const { flatMapKey } = this.buildRemoteCacheKeys({ workspaceId });
await this.cacheStorageService.set(flatMapKey, flatMap);
}
async flushCache({ workspaceId }: { workspaceId: string }): Promise<void> {
const { flatMapKey, hashKey } = this.buildRemoteCacheKeys({ workspaceId });
await this.cacheStorageService.del(flatMapKey);
await this.cacheStorageService.del(hashKey);
this.localCacheFlatMaps.delete(workspaceId);
this.localCacheHashes.delete(workspaceId);
}
}
@@ -1,57 +0,0 @@
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
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 { WorkspaceFlatMapCacheRegistryService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache-registry.service';
@Module({
imports: [
DiscoveryModule,
TypeOrmModule.forFeature([
ViewEntity,
ViewFieldEntity,
ViewFilterEntity,
ViewGroupEntity,
IndexMetadataEntity,
IndexFieldMetadataEntity,
FieldMetadataEntity,
ObjectMetadataEntity,
]),
],
providers: [
WorkspaceFlatMapCacheRegistryService,
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
],
exports: [
WorkspaceFlatMapCacheRegistryService,
WorkspaceFlatObjectMetadataMapCacheService,
WorkspaceFlatViewMapCacheService,
WorkspaceFlatViewFieldMapCacheService,
WorkspaceFlatViewFilterMapCacheService,
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
],
})
export class WorkspaceFlatMapCacheModule {}
@@ -17,6 +17,7 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE
return {
summary: {
invalidAgent: 0,
invalidViewFilter: 0,
invalidObjectMetadata: 0,
invalidView: 0,
@@ -14,6 +14,7 @@ import {
WorkspaceMigrationOrchestratorSuccessfulResult,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util';
import { WorkspaceMigrationV2AgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/workspace-migration-v2-agent-actions-builder.service';
import { WorkspaceMigrationV2CronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/cron-trigger/workspace-migration-v2-cron-trigger-action-builder.service';
import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/database-event-trigger/workspace-migration-v2-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
@@ -44,6 +45,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationV2RoleTargetActionsBuilderService: WorkspaceMigrationV2RoleTargetActionsBuilderService,
private readonly workspaceMigrationV2FieldActionsBuilderService: WorkspaceMigrationV2FieldActionsBuilderService,
private readonly workspaceMigrationV2RoleActionsBuilderService: WorkspaceMigrationV2RoleActionsBuilderService,
private readonly workspaceMigrationV2AgentActionsBuilderService: WorkspaceMigrationV2AgentActionsBuilderService,
) {}
private setupOptimisticCache({
@@ -131,6 +133,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatViewGroupMaps,
flatRoleMaps,
flatRoleTargetMaps,
flatAgentMaps,
} = fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
@@ -578,6 +581,33 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatAgentMaps)) {
const { from: fromFlatAgentMaps, to: toFlatAgentMaps } = flatAgentMaps;
const agentResult =
this.workspaceMigrationV2AgentActionsBuilderService.validateAndBuild({
from: fromFlatAgentMaps,
to: toFlatAgentMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: undefined,
workspaceId,
});
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
agentResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (agentResult.status === 'fail') {
orchestratorFailureReport.agent.push(...agentResult.errors);
} else {
orchestratorActionsReport.agent = agentResult.actions;
}
}
const allErrors = Object.values(orchestratorFailureReport);
if (allErrors.some((report) => report.length > 0)) {
@@ -666,6 +696,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.roleTarget.created,
...aggregatedOrchestratorActionsReport.roleTarget.updated,
///
//
...aggregatedOrchestratorActionsReport.agent.deleted,
...aggregatedOrchestratorActionsReport.agent.created,
...aggregatedOrchestratorActionsReport.agent.updated,
///
],
workspaceId,
},
@@ -0,0 +1,18 @@
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
export type UpdateAgentAction = {
type: 'update_agent';
flatEntityId: string;
flatEntityUpdates: FlatEntityPropertiesUpdates<'agent'>;
};
export type CreateAgentAction = {
type: 'create_agent';
flatEntity: FlatAgent;
};
export type DeleteAgentAction = {
type: 'delete_agent';
flatEntityId: string;
};
@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateAgentAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/types/workspace-migration-v2-agent-action-builder.service';
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-agent-validator.service';
@Injectable()
export class WorkspaceMigrationV2AgentActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
typeof ALL_METADATA_NAME.agent
> {
constructor(
private readonly flatAgentValidatorService: FlatAgentValidatorService,
) {
super(ALL_METADATA_NAME.agent);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.agent>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.agent, 'created'> {
const validationResult =
this.flatAgentValidatorService.validateFlatAgentCreation(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatAgentToValidate } = args;
return {
status: 'success',
action: {
type: 'create_agent',
flatEntity: flatAgentToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.agent>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.agent, 'deleted'> {
const validationResult =
this.flatAgentValidatorService.validateFlatAgentDeletion(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatAgentToValidate } = args;
return {
status: 'success',
action: {
type: 'delete_agent',
flatEntityId: flatAgentToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.agent>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.agent, 'updated'> {
const validationResult =
this.flatAgentValidatorService.validateFlatAgentUpdate(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateAgentAction: UpdateAgentAction = {
type: 'update_agent',
flatEntityId,
flatEntityUpdates,
};
return {
status: 'success',
action: updateAgentAction,
};
}
}
@@ -0,0 +1,193 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { validateAgentNameUniqueness } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-agent-name-uniqueness.util';
import { validateAgentRequiredProperties } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-agent-required-properties.util';
import { validateAgentResponseFormat } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-agent-response-format.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class FlatAgentValidatorService {
public validateFlatAgentCreation({
flatEntityToValidate: flatAgent,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatAgentMaps: optimisticFlatAgentMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.agent
>): FailedFlatEntityValidation<FlatAgent> {
const validationResult: FailedFlatEntityValidation<FlatAgent> = {
type: 'create_agent',
errors: [],
flatEntityMinimalInformation: {
id: flatAgent.id,
name: flatAgent.name,
},
};
const existingAgents = Object.values(optimisticFlatAgentMaps.byId).filter(
isDefined,
);
validationResult.errors.push(
...validateAgentRequiredProperties({
flatAgent,
}),
);
validationResult.errors.push(
...validateAgentNameUniqueness({
name: flatAgent.name,
existingFlatAgents: existingAgents,
}),
);
if (isDefined(flatAgent.responseFormat)) {
validationResult.errors.push(
...validateAgentResponseFormat({
responseFormat: flatAgent.responseFormat,
}),
);
}
return validationResult;
}
public validateFlatAgentDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatAgentMaps: optimisticFlatAgentMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.agent
>): FailedFlatEntityValidation<FlatAgent> {
const validationResult: FailedFlatEntityValidation<FlatAgent> = {
type: 'delete_agent',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
name: flatEntityToValidate.name,
},
};
const existingAgent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityToValidate.id,
flatEntityMaps: optimisticFlatAgentMaps,
});
if (!isDefined(existingAgent)) {
validationResult.errors.push({
code: AgentExceptionCode.AGENT_NOT_FOUND,
message: t`Agent not found`,
userFriendlyMessage: msg`Agent not found`,
});
return validationResult;
}
if (
existingAgent.isCustom === false &&
isDefined(existingAgent.standardId)
) {
validationResult.errors.push({
code: AgentExceptionCode.AGENT_IS_STANDARD,
message: t`Cannot delete standard agent`,
userFriendlyMessage: msg`Cannot delete standard agent`,
});
}
return validationResult;
}
public validateFlatAgentUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatAgentMaps: optimisticFlatAgentMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.agent
>): FailedFlatEntityValidation<FlatAgent> {
const validationResult: FailedFlatEntityValidation<FlatAgent> = {
type: 'update_agent',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityId,
},
};
const fromFlatAgent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId,
flatEntityMaps: optimisticFlatAgentMaps,
});
if (!isDefined(fromFlatAgent)) {
validationResult.errors.push({
code: AgentExceptionCode.AGENT_NOT_FOUND,
message: t`Agent not found`,
userFriendlyMessage: msg`Agent not found`,
});
return validationResult;
}
if (
fromFlatAgent.isCustom === false &&
isDefined(fromFlatAgent.standardId)
) {
validationResult.errors.push({
code: AgentExceptionCode.AGENT_IS_STANDARD,
message: t`Cannot update standard agent`,
userFriendlyMessage: msg`Cannot update standard agent`,
});
}
const partialFlatAgent: Partial<FlatAgent> =
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
});
const optimisticFlatAgent: FlatAgent = {
...fromFlatAgent,
...partialFlatAgent,
};
const existingAgents = Object.values(optimisticFlatAgentMaps.byId)
.filter(isDefined)
.filter((agent) => agent.id !== flatEntityId);
validationResult.errors.push(
...validateAgentRequiredProperties({
flatAgent: optimisticFlatAgent,
}),
);
if (isDefined(partialFlatAgent.name)) {
validationResult.errors.push(
...validateAgentNameUniqueness({
name: partialFlatAgent.name,
existingFlatAgents: existingAgents,
}),
);
}
if (isDefined(partialFlatAgent.responseFormat)) {
validationResult.errors.push(
...validateAgentResponseFormat({
responseFormat: partialFlatAgent.responseFormat,
}),
);
}
return validationResult;
}
}
@@ -0,0 +1,25 @@
import { msg, t } from '@lingui/core/macro';
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateAgentNameUniqueness = ({
name,
existingFlatAgents,
}: {
name: string;
existingFlatAgents: FlatAgent[];
}): FlatEntityValidationError<AgentExceptionCode>[] => {
const errors: FlatEntityValidationError<AgentExceptionCode>[] = [];
if (existingFlatAgents.some((agent) => agent.name === name)) {
errors.push({
code: AgentExceptionCode.AGENT_ALREADY_EXISTS,
message: t`Agent with name "${name}" already exists`,
userFriendlyMessage: msg`An agent with this name already exists`,
});
}
return errors;
};
@@ -0,0 +1,40 @@
import { isNonEmptyString } from '@sniptt/guards';
import { msg, t } from '@lingui/core/macro';
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateAgentRequiredProperties = ({
flatAgent,
}: {
flatAgent: FlatAgent;
}): FlatEntityValidationError<AgentExceptionCode>[] => {
const errors: FlatEntityValidationError<AgentExceptionCode>[] = [];
if (!isNonEmptyString(flatAgent.label)) {
errors.push({
code: AgentExceptionCode.INVALID_AGENT_INPUT,
message: t`Label cannot be empty`,
userFriendlyMessage: msg`Label cannot be empty`,
});
}
if (!isNonEmptyString(flatAgent.prompt)) {
errors.push({
code: AgentExceptionCode.INVALID_AGENT_INPUT,
message: t`Prompt cannot be empty`,
userFriendlyMessage: msg`Prompt cannot be empty`,
});
}
if (!isNonEmptyString(flatAgent.modelId)) {
errors.push({
code: AgentExceptionCode.INVALID_AGENT_INPUT,
message: t`Model ID cannot be empty`,
userFriendlyMessage: msg`Model ID cannot be empty`,
});
}
return errors;
};
@@ -0,0 +1,47 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import {
type AgentResponseFormat,
type AgentJsonResponseFormat,
} from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateAgentResponseFormat = ({
responseFormat,
}: {
responseFormat: AgentResponseFormat;
}): FlatEntityValidationError<AgentExceptionCode>[] => {
const errors: FlatEntityValidationError<AgentExceptionCode>[] = [];
const type = responseFormat.type;
if (type !== 'text' && type !== 'json') {
errors.push({
code: AgentExceptionCode.INVALID_AGENT_INPUT,
message: t`Response format type must be either "text" or "json"`,
userFriendlyMessage: msg`Invalid response format type`,
});
}
if (type === 'json' && !isDefined(responseFormat.schema)) {
errors.push({
code: AgentExceptionCode.INVALID_AGENT_INPUT,
message: t`Response format with type "json" must include a schema`,
userFriendlyMessage: msg`JSON response format requires a schema`,
});
}
if (
type === 'text' &&
isDefined((responseFormat as unknown as AgentJsonResponseFormat).schema)
) {
errors.push({
code: AgentExceptionCode.INVALID_AGENT_INPUT,
message: t`Response format with type "text" should not include a schema`,
userFriendlyMessage: msg`Text response format should not have a schema`,
});
}
return errors;
};
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-agent-validator.service';
import { FlatCronTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-cron-trigger-validator.service';
import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-database-event-trigger-validator.service';
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service';
@@ -33,6 +34,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
FlatAgentValidatorService,
],
exports: [
FlatViewValidatorService,
@@ -49,6 +51,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
FlatAgentValidatorService,
],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { WorkspaceMigrationV2AgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/workspace-migration-v2-agent-actions-builder.service';
import { WorkspaceMigrationV2CronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/cron-trigger/workspace-migration-v2-cron-trigger-action-builder.service';
import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/database-event-trigger/workspace-migration-v2-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
@@ -34,6 +35,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2RouteTriggerActionsBuilderService,
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
WorkspaceMigrationV2AgentActionsBuilderService,
],
exports: [
WorkspaceMigrationV2ViewActionsBuilderService,
@@ -49,6 +51,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2RouteTriggerActionsBuilderService,
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
WorkspaceMigrationV2AgentActionsBuilderService,
FlatFieldMetadataTypeValidatorService,
],
})
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { CreateAgentAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/types/workspace-migration-v2-agent-action-builder.service';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateAgentActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_agent',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateAgentAction>): Partial<AllFlatEntityMaps> {
const { flatAgentMaps } = allFlatEntityMaps;
const { flatEntity } = action;
const updatedFlatAgentMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity,
flatEntityMaps: flatAgentMaps,
});
return {
flatAgentMaps: updatedFlatAgentMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateAgentAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntity } = action;
const agentRepository =
queryRunner.manager.getRepository<AgentEntity>(AgentEntity);
await agentRepository.save({
...flatEntity,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateAgentAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { DeleteAgentAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/types/workspace-migration-v2-agent-action-builder.service';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteAgentActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_agent',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteAgentAction>): Partial<AllFlatEntityMaps> {
const { flatAgentMaps } = allFlatEntityMaps;
const { flatEntityId } = action;
const updatedFlatAgentMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: flatEntityId,
flatEntityMaps: flatAgentMaps,
});
return {
flatAgentMaps: updatedFlatAgentMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteAgentAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntityId } = action;
const agentRepository =
queryRunner.manager.getRepository<AgentEntity>(AgentEntity);
await agentRepository.delete({ id: flatEntityId, workspaceId });
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteAgentAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { UpdateAgentAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/types/workspace-migration-v2-agent-action-builder.service';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateAgentActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_agent',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateAgentAction>): Partial<AllFlatEntityMaps> {
const { flatAgentMaps } = allFlatEntityMaps;
const { flatEntityId, flatEntityUpdates } = action;
const existingAgent = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId,
flatEntityMaps: flatAgentMaps,
});
const updatedAgent = {
...existingAgent,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
const updatedFlatAgentMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedAgent,
flatEntityMaps: flatAgentMaps,
});
return {
flatAgentMaps: updatedFlatAgentMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateAgentAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntityId, flatEntityUpdates } = action;
const agentRepository =
queryRunner.manager.getRepository<AgentEntity>(AgentEntity);
await agentRepository.update(
{ id: flatEntityId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateAgentAction>,
): Promise<void> {
return;
}
}
@@ -1,6 +1,9 @@
import { Module } from '@nestjs/common';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/create-agent-action-handler.service';
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/delete-agent-action-handler.service';
import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/update-agent-action-handler.service';
import { CreateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/create-cron-trigger-action-handler.service';
import { DeleteCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/delete-cron-trigger-action-handler.service';
import { UpdateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/update-cron-trigger-action-handler.service';
@@ -93,6 +96,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
CreateRoleTargetActionHandlerService,
DeleteRoleTargetActionHandlerService,
UpdateRoleTargetActionHandlerService,
CreateAgentActionHandlerService,
UpdateAgentActionHandlerService,
DeleteAgentActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -122,7 +122,9 @@ export class WorkspaceMigrationRunnerV2Service {
this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'rolesPermissions',
'userWorkspaceRoleMap',
'flatRoleTargetMaps',
'apiKeyRoleMap',
'flatRoleTargetByAgentIdMaps',
]),
);
}
@@ -16,28 +16,18 @@ export class StandardAgentFactory {
): FlatAgent[] {
const computedAgents: FlatAgent[] = [];
for (const agentDefinition of agentDefinitions) {
const existingAgent = existingAgents.find(
(agent) => agent.standardId === agentDefinition.standardId,
for (const standardAgentDefinition of agentDefinitions) {
const existingAgentEntity = existingAgents.find(
(agent) => agent.standardId === standardAgentDefinition.standardId,
);
const flatAgent = transformStandardAgentDefinitionToFlatAgent(
agentDefinition,
context.workspaceId,
);
const flatAgent = transformStandardAgentDefinitionToFlatAgent({
standardAgentDefinition,
workspaceId: context.workspaceId,
existingAgentEntity,
});
if (existingAgent) {
computedAgents.push({
...flatAgent,
id: existingAgent.id,
universalIdentifier: agentDefinition.standardId,
});
} else {
computedAgents.push({
...flatAgent,
universalIdentifier: agentDefinition.standardId,
});
}
computedAgents.push(flatAgent);
}
return computedAgents;
@@ -4,7 +4,13 @@ export type AgentOutputStrategy = 'direct' | 'synthesize';
export type StandardAgentDefinition = Omit<
FlatAgent,
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
| 'id'
| 'workspaceId'
| 'universalIdentifier'
| 'standardId'
| 'updatedAt'
| 'deletedAt'
| 'createdAt'
> & {
standardId: string;
standardRoleId?: string;