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
@@ -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,
};
};