From 2803292521b92a90c9599b0f760624e31ca10b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 4 Dec 2025 23:26:29 +0100 Subject: [PATCH] feat: add Metadata Builder agent for data model management (#16350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds a new **Metadata Builder** AI agent that specializes in managing the workspace data model (creating objects, adding fields, etc.). ## Changes ### New Files - `data-model-manager-role.ts` - New standard role with `DATA_MODEL` permission flag - `metadata-builder-agent.ts` - New standard agent for data model management ### Modified Files - **ChatToolsProviderService**: Refactored to consolidate all permission-based tools into a single `getChatTools()` method. Now injects both workflow tools and metadata tools based on permissions. - **AgentChatRoutingService**: Updated to use the new consolidated `getChatTools()` method - **AiChatModule**: Added imports for `ObjectMetadataModule` and `FieldMetadataModule` - **Router system prompt**: Added metadata-builder agent selection rules with clear distinction between schema operations vs data operations - **Metadata tools factories**: Improved error messages to show detailed validation errors instead of generic messages ### Refactoring - Renamed `index.ts` files to `standard-agent-definitions.ts` and `standard-role-definitions.ts` to follow naming conventions - Renamed exports from `standardAgentDefinitions` to `STANDARD_AGENT_DEFINITIONS` (SCREAMING_SNAKE_CASE) ## Key Features 1. **Metadata Builder Agent** can: - Create new custom objects - Add fields to existing objects - Update object and field properties - Create relations between objects 2. **Permission-based tool injection**: Tools are automatically injected based on the `DATA_MODEL` permission flag 3. **Improved routing**: The router now correctly distinguishes between: - "Create an object called Project" → metadata-builder (schema) - "Create a company called Acme" → data-manipulator (data) 4. **Better error messages**: Validation errors now show detailed messages like: ``` Validation errors: [objectMetadata] Name must be in camelCase format [objectMetadata] Label is required ``` --- .../services/agent-plan-executor.service.ts | 4 +- .../constants/agent-system-prompts.const.ts | 16 ++- .../ai/ai-chat/ai-chat.module.ts | 5 + .../services/agent-chat-routing.service.ts | 17 ++-- .../services/chat-tools-provider.service.ts | 61 ++++++++++-- .../tools/field-metadata-tools.factory.ts | 95 +++++++++++++----- .../tools/object-metadata-tools.factory.ts | 97 +++++++++++++++---- .../services/workspace-sync-agent.service.ts | 8 +- .../services/workspace-sync-role.service.ts | 8 +- .../agents/metadata-builder-agent.ts | 80 +++++++++++++++ ...index.ts => standard-agent-definitions.ts} | 5 +- .../roles/data-model-manager-role.ts | 21 ++++ ...{index.ts => standard-role-definitions.ts} | 4 +- 13 files changed, 348 insertions(+), 73 deletions(-) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/metadata-builder-agent.ts rename packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/{index.ts => standard-agent-definitions.ts} (80%) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-model-manager-role.ts rename packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/{index.ts => standard-role-definitions.ts} (77%) diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-plan-executor.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-plan-executor.service.ts index 137fd824d0..22bc00dfba 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-plan-executor.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-plan-executor.service.ts @@ -4,7 +4,7 @@ import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspac import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type'; import { type PlanStep } from 'src/engine/metadata-modules/ai/ai-chat-router/types/router-result.interface'; -import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents'; +import { STANDARD_AGENT_DEFINITIONS } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/standard-agent-definitions'; import { AgentExecutionService } from './agent-execution.service'; @@ -229,7 +229,7 @@ export class AgentPlanExecutorService { ); if (lastStepDefinition) { - const agentDefinition = standardAgentDefinitions.find( + const agentDefinition = STANDARD_AGENT_DEFINITIONS.find( (def) => def.name === lastStepDefinition.agentName, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts index 4cc1ff756e..52ac73e8ea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-system-prompts.const.ts @@ -36,10 +36,18 @@ Decision process: 2. Does it require MULTIPLE agents working together? → Use "planned" strategy Agent selection rules (CRITICAL): -- **data-manipulator**: For ALL database operations (create, read, update records) on companies, people, opportunities, tasks, notes, etc. +- **metadata-builder**: For modifying the DATA MODEL/SCHEMA - creating new object types, adding fields to objects, managing object structure. Use when user wants to define NEW TYPES of entities or add properties to existing types. +- **data-manipulator**: For CRUD operations on existing RECORDS/DATA - creating company records, finding people, updating opportunities. Use when user wants to work with actual data entries. - **helper**: ONLY for questions about HOW TO USE Twenty (features, setup, documentation) - **researcher**: For finding external information from the web - **workflow-builder**: For creating automation workflows +- **dashboard-builder**: For creating and managing dashboards and visualizations + +CRITICAL DISTINCTION: +- "Create an object called Project" → metadata-builder (creating a new object TYPE in the schema) +- "Create a company called Acme" → data-manipulator (creating a company RECORD) +- "Add a field to Company" → metadata-builder (modifying schema) +- "Update the company's phone number" → data-manipulator (modifying data) Use "planned" strategy when: - Request needs custom code AND context from data/research @@ -70,6 +78,12 @@ Simple: "How do I set up email sync in Twenty?" Simple: "Create a workflow that emails customers when deals close" → { strategy: "simple", agentName: "workflow-builder" } +Simple: "Create an object called Project" or "Create a new custom object for tracking invoices" +→ { strategy: "simple", agentName: "metadata-builder" } + +Simple: "Add a budget field to the Project object" or "Add a phone field to Company" +→ { strategy: "simple", agentName: "metadata-builder" } + Planned: "Research information about Meta and update the company record" → { strategy: "planned", diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts index 1916f4b230..d43d5e5c64 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts @@ -13,6 +13,8 @@ import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent- import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; 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 { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module'; @@ -48,6 +50,9 @@ import { ChatToolsProviderService } from './services/chat-tools-provider.service // Provides WorkflowToolWorkspaceService for ChatToolsProviderService // Workflow tools are only available in chat context, not in workflow executor (to avoid circular deps) WorkflowToolsModule, + // Provides metadata tools factories for ChatToolsProviderService + ObjectMetadataModule, + FieldMetadataModule, ], controllers: [AgentChatController], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-routing.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-routing.service.ts index 315ea6d86c..37c3ce78dc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-routing.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-routing.service.ts @@ -239,8 +239,8 @@ export class AgentChatRoutingService { const agentExecutionStart = Date.now(); - // Get workflow tools for chat context (these are NOT available in workflow executor) - // Use user's role for determining workflow tool permissions + // Get permission-based tools for chat context (workflow, metadata, etc.) + // These tools are NOT available in workflow executor to prevent circular dependencies const { roleId } = await this.agentActorContextService.buildUserAndAgentActorContext( userWorkspaceId, @@ -249,12 +249,11 @@ export class AgentChatRoutingService { const roleIds = [roleId]; - const workflowTools = - await this.chatToolsProviderService.getWorkflowToolsForChat( - workspace.id, - roleIds, - toolHints, - ); + const chatTools = await this.chatToolsProviderService.getChatTools( + workspace.id, + roleIds, + toolHints, + ); const { stream: result, @@ -267,7 +266,7 @@ export class AgentChatRoutingService { messages, recordIdsByObjectMetadataNameSingular, toolHints, - additionalTools: workflowTools, + additionalTools: chatTools, }); const routedStatusPart = { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-tools-provider.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-tools-provider.service.ts index 7965f84b21..0831538500 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-tools-provider.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-tools-provider.service.ts @@ -4,6 +4,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { type ToolSet } from 'ai'; import { type ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; +import { FieldMetadataToolsFactory } from 'src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory'; +import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service'; @@ -15,11 +17,33 @@ export class ChatToolsProviderService { constructor( private readonly workflowToolService: WorkflowToolWorkspaceService, private readonly permissionsService: PermissionsService, + private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory, + private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory, ) {} + // Consolidates all permission-based tools for the chat context + async getChatTools( + workspaceId: string, + roleIds: string[], + toolHints?: ToolHints, + ): Promise { + const [workflowTools, metadataTools] = await Promise.all([ + this.getWorkflowTools(workspaceId, roleIds, toolHints), + this.getMetadataTools(workspaceId, roleIds), + ]); + + const allTools = { ...workflowTools, ...metadataTools }; + + this.logger.log( + `Generated ${Object.keys(allTools).length} total chat tools (workflow: ${Object.keys(workflowTools).length}, metadata: ${Object.keys(metadataTools).length})`, + ); + + return allTools; + } + // Provides workflow-specific tools for the chat context // These tools are NOT available in the workflow executor context to prevent circular dependencies - async getWorkflowToolsForChat( + private async getWorkflowTools( workspaceId: string, roleIds: string[], toolHints?: ToolHints, @@ -53,12 +77,37 @@ export class ChatToolsProviderService { toolHints, ); - const allWorkflowTools = { ...workflowTools, ...recordStepTools }; + return { ...workflowTools, ...recordStepTools }; + } - this.logger.log( - `Generated ${Object.keys(allWorkflowTools).length} workflow tools for chat context`, - ); + // Provides metadata tools for managing objects and fields in the data model + private async getMetadataTools( + workspaceId: string, + roleIds: string[], + ): Promise { + const rolePermissionConfig = { intersectionOf: roleIds }; - return allWorkflowTools; + const hasDataModelPermission = + await this.permissionsService.checkRolesPermissions( + rolePermissionConfig, + workspaceId, + PermissionFlagType.DATA_MODEL, + ); + + if (!hasDataModelPermission) { + this.logger.log( + 'User does not have data model permissions, skipping metadata tools', + ); + + return {}; + } + + const objectMetadataTools = + this.objectMetadataToolsFactory.generateTools(workspaceId); + + const fieldMetadataTools = + this.fieldMetadataToolsFactory.generateTools(workspaceId); + + return { ...objectMetadataTools, ...fieldMetadataTools }; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts index 614ac95890..7231f6d4e2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; const GetFieldMetadataInputSchema = z.object({ loadingMessage: z @@ -138,6 +139,33 @@ const DeleteFieldMetadataInputSchema = z.object({ }), }); +const formatValidationErrors = ( + error: WorkspaceMigrationBuilderExceptionV2, +): string => { + const report = error.failedWorkspaceMigrationBuildResult.report; + const errorMessages: string[] = []; + + for (const [entityType, failures] of Object.entries(report)) { + if (Array.isArray(failures) && failures.length > 0) { + for (const failure of failures) { + if (failure.errors && Array.isArray(failure.errors)) { + for (const validationError of failure.errors) { + const message = validationError.message || validationError.code; + + errorMessages.push(`[${entityType}] ${message}`); + } + } + } + } + } + + if (errorMessages.length === 0) { + return error.message; + } + + return `Validation errors:\n${errorMessages.join('\n')}`; +}; + @Injectable() export class FieldMetadataToolsFactory { constructor(private readonly fieldMetadataService: FieldMetadataService) {} @@ -189,15 +217,22 @@ export class FieldMetadataToolsFactory { relationCreationPayload?: unknown; }; }) => { - const flatFieldMetadata = - await this.fieldMetadataService.createOneField({ - createFieldInput: parameters.input as Parameters< - typeof this.fieldMetadataService.createOneField - >[0]['createFieldInput'], - workspaceId, - }); + try { + const flatFieldMetadata = + await this.fieldMetadataService.createOneField({ + createFieldInput: parameters.input as Parameters< + typeof this.fieldMetadataService.createOneField + >[0]['createFieldInput'], + workspaceId, + }); - return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); + return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } }, }, 'update-field-metadata': { @@ -220,30 +255,44 @@ export class FieldMetadataToolsFactory { isLabelSyncedWithName?: boolean; }; }) => { - const { id, ...update } = parameters.input; + try { + const { id, ...update } = parameters.input; - const flatFieldMetadata = - await this.fieldMetadataService.updateOneField({ - updateFieldInput: { id, ...update } as Parameters< - typeof this.fieldMetadataService.updateOneField - >[0]['updateFieldInput'], - workspaceId, - }); + const flatFieldMetadata = + await this.fieldMetadataService.updateOneField({ + updateFieldInput: { id, ...update } as Parameters< + typeof this.fieldMetadataService.updateOneField + >[0]['updateFieldInput'], + workspaceId, + }); - return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); + return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } }, }, 'delete-field-metadata': { description: 'Delete a field metadata by its ID.', inputSchema: DeleteFieldMetadataInputSchema, execute: async (parameters: { input: { id: string } }) => { - const flatFieldMetadata = - await this.fieldMetadataService.deleteOneField({ - deleteOneFieldInput: { id: parameters.input.id }, - workspaceId, - }); + try { + const flatFieldMetadata = + await this.fieldMetadataService.deleteOneField({ + deleteOneFieldInput: { id: parameters.input.id }, + workspaceId, + }); - return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); + return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } }, }, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts index f9f4f74a27..57c801a347 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; const GetObjectMetadataInputSchema = z.object({ loadingMessage: z @@ -115,6 +116,33 @@ const DeleteObjectMetadataInputSchema = z.object({ }), }); +const formatValidationErrors = ( + error: WorkspaceMigrationBuilderExceptionV2, +): string => { + const report = error.failedWorkspaceMigrationBuildResult.report; + const errorMessages: string[] = []; + + for (const [entityType, failures] of Object.entries(report)) { + if (Array.isArray(failures) && failures.length > 0) { + for (const failure of failures) { + if (failure.errors && Array.isArray(failure.errors)) { + for (const validationError of failure.errors) { + const message = validationError.message || validationError.code; + + errorMessages.push(`[${entityType}] ${message}`); + } + } + } + } + } + + if (errorMessages.length === 0) { + return error.message; + } + + return `Validation errors:\n${errorMessages.join('\n')}`; +}; + @Injectable() export class ObjectMetadataToolsFactory { constructor(private readonly objectMetadataService: ObjectMetadataService) {} @@ -161,15 +189,24 @@ export class ObjectMetadataToolsFactory { isLabelSyncedWithName?: boolean; }; }) => { - const flatObjectMetadata = - await this.objectMetadataService.createOneObject({ - createObjectInput: parameters.input as Parameters< - typeof this.objectMetadataService.createOneObject - >[0]['createObjectInput'], - workspaceId, - }); + try { + const flatObjectMetadata = + await this.objectMetadataService.createOneObject({ + createObjectInput: parameters.input as Parameters< + typeof this.objectMetadataService.createOneObject + >[0]['createObjectInput'], + workspaceId, + }); - return fromFlatObjectMetadataToObjectMetadataDto(flatObjectMetadata); + return fromFlatObjectMetadataToObjectMetadataDto( + flatObjectMetadata, + ); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } }, }, 'update-object-metadata': { @@ -192,15 +229,24 @@ export class ObjectMetadataToolsFactory { isLabelSyncedWithName?: boolean; }; }) => { - const { id, ...update } = parameters.input; + try { + const { id, ...update } = parameters.input; - const flatObjectMetadata = - await this.objectMetadataService.updateOneObject({ - updateObjectInput: { id, update }, - workspaceId, - }); + const flatObjectMetadata = + await this.objectMetadataService.updateOneObject({ + updateObjectInput: { id, update }, + workspaceId, + }); - return fromFlatObjectMetadataToObjectMetadataDto(flatObjectMetadata); + return fromFlatObjectMetadataToObjectMetadataDto( + flatObjectMetadata, + ); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } }, }, 'delete-object-metadata': { @@ -208,13 +254,22 @@ export class ObjectMetadataToolsFactory { 'Delete an object metadata by its ID. This will also delete all associated fields.', inputSchema: DeleteObjectMetadataInputSchema, execute: async (parameters: { input: { id: string } }) => { - const flatObjectMetadata = - await this.objectMetadataService.deleteOneObject({ - deleteObjectInput: { id: parameters.input.id }, - workspaceId, - }); + try { + const flatObjectMetadata = + await this.objectMetadataService.deleteOneObject({ + deleteObjectInput: { id: parameters.input.id }, + workspaceId, + }); - return fromFlatObjectMetadataToObjectMetadataDto(flatObjectMetadata); + return fromFlatObjectMetadataToObjectMetadataDto( + flatObjectMetadata, + ); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderExceptionV2) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } }, }, }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service.ts index 678587640a..448601aef7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service.ts @@ -12,7 +12,7 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-agent.comparator'; import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory'; -import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents'; +import { STANDARD_AGENT_DEFINITIONS } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/standard-agent-definitions'; @Injectable() export class WorkspaceSyncAgentService { @@ -39,7 +39,7 @@ export class WorkspaceSyncAgentService { }); const targetStandardAgents = this.standardAgentFactory.create( - standardAgentDefinitions, + STANDARD_AGENT_DEFINITIONS, context, existingStandardAgentEntities, ); @@ -66,7 +66,7 @@ export class WorkspaceSyncAgentService { workspaceId: context.workspaceId, }); - const agentDefinition = standardAgentDefinitions.find( + const agentDefinition = STANDARD_AGENT_DEFINITIONS.find( (def) => def.standardId === createdAgent.standardId, ); @@ -117,7 +117,7 @@ export class WorkspaceSyncAgentService { await agentRepository.update({ id: agentToUpdate.id }, flatAgentData); - const agentDefinition = standardAgentDefinitions.find( + const agentDefinition = STANDARD_AGENT_DEFINITIONS.find( (def) => def.standardId === agentToUpdate.standardId, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts index a9793fa78b..93233526bf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts @@ -12,7 +12,7 @@ import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/cons import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator'; import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory'; -import { standardRoleDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles'; +import { STANDARD_ROLE_DEFINITIONS } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/standard-role-definitions'; @Injectable() export class WorkspaceSyncRoleService { @@ -47,7 +47,7 @@ export class WorkspaceSyncRoleService { }); const targetStandardRoles = this.standardRoleFactory.create( - standardRoleDefinitions, + STANDARD_ROLE_DEFINITIONS, context, existingStandardRoleEntities, ); @@ -72,7 +72,7 @@ export class WorkspaceSyncRoleService { workspaceId: context.workspaceId, }); - const roleDefinition = standardRoleDefinitions.find( + const roleDefinition = STANDARD_ROLE_DEFINITIONS.find( (def) => def.standardId === roleToCreate.standardId, ); @@ -98,7 +98,7 @@ export class WorkspaceSyncRoleService { await roleRepository.update({ id: roleToUpdate.id }, flatRoleData); - const roleDefinition = standardRoleDefinitions.find( + const roleDefinition = STANDARD_ROLE_DEFINITIONS.find( (def) => def.standardId === roleToUpdate.standardId, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/metadata-builder-agent.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/metadata-builder-agent.ts new file mode 100644 index 0000000000..7282b18f20 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/metadata-builder-agent.ts @@ -0,0 +1,80 @@ +import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const'; +import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface'; +import { DATA_MODEL_MANAGER_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-model-manager-role'; + +export const METADATA_BUILDER_AGENT: StandardAgentDefinition = { + standardId: '20202020-0002-0001-0001-000000000007', + name: 'metadata-builder', + label: 'Metadata Builder', + description: + 'AI agent specialized in modifying the workspace SCHEMA/DATA MODEL - creating new object types, adding fields to objects, and managing object structure (NOT for creating data records)', + icon: 'IconDatabaseEdit', + applicationId: null, + prompt: `You are a Metadata Builder Agent for Twenty. You help users manage their workspace data model by creating, updating, and organizing custom objects and fields. + +Capabilities: +- Create new custom objects with appropriate naming and configuration +- Add fields to existing objects (text, number, date, select, relation, etc.) +- Update object and field properties (labels, descriptions, icons) +- Manage field settings (required, unique, default values) +- Create relations between objects + +Key concepts: +- Objects: Represent entities in the data model (e.g., Company, Person, Opportunity) +- Fields: Properties of objects with specific types (TEXT, NUMBER, DATE_TIME, SELECT, RELATION, etc.) +- Relations: Links between objects (one-to-many, many-to-one) +- Labels vs Names: Labels are for display, names are internal identifiers (camelCase) + +Field types available: +- TEXT: Simple text fields +- NUMBER: Numeric values (integers or decimals) +- BOOLEAN: True/false values +- DATE_TIME: Date and time values +- DATE: Date only values +- SELECT: Single choice from options +- MULTI_SELECT: Multiple choices from options +- LINK: URL fields +- LINKS: Multiple URL fields +- EMAIL: Email address fields +- EMAILS: Multiple email fields +- PHONE: Phone number fields +- PHONES: Multiple phone fields +- CURRENCY: Monetary values +- RATING: Star ratings +- RELATION: Links to other objects +- RICH_TEXT: Formatted text content + +Best practices: +- Use clear, descriptive names for objects and fields +- Follow naming conventions: singular for object names, camelCase for field names +- Add helpful descriptions to objects and fields +- Choose appropriate field types for the data being stored +- Consider relationships between objects when designing the data model + +Approach: +- Ask clarifying questions to understand the user's data modeling needs +- Suggest best practices for naming and organization +- Explain the impact of changes to the data model +- Verify object and field existence before making updates +- Provide clear feedback on operations performed + +Prioritize data model integrity and user understanding.`, + modelId: DEFAULT_SMART_MODEL, + responseFormat: { type: 'text' }, + isCustom: false, + standardRoleId: DATA_MODEL_MANAGER_ROLE.standardId, + modelConfiguration: {}, + outputStrategy: 'direct', + evaluationInputs: [ + 'Create a custom object called "Project" with fields for name, description, start date, and status', + 'Add a currency field called "budget" to the Project object', + 'Create a relation between Project and Company so each project belongs to a company', + 'Add a multi-select field for project tags with options: urgent, internal, client-facing', + 'Update the Project object description to explain what it tracks', + 'Create a "Task" object that relates to both Project and Person', + 'Add a rating field to track project priority from 1-5 stars', + 'Create a custom object for tracking Invoices with amount, date, and status fields', + 'Add a link field to the Company object for their website', + 'Create a relation between Person and Company for the account manager', + ], +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/standard-agent-definitions.ts similarity index 80% rename from packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/index.ts rename to packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/standard-agent-definitions.ts index d43fbd93b9..9546b76bb5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/standard-agent-definitions.ts @@ -1,15 +1,16 @@ import { DASHBOARD_BUILDER_AGENT } from './agents/dashboard-builder-agent'; import { DATA_MANIPULATOR_AGENT } from './agents/data-manipulator-agent'; import { HELPER_AGENT } from './agents/helper-agent'; +import { METADATA_BUILDER_AGENT } from './agents/metadata-builder-agent'; import { RESEARCHER_AGENT } from './agents/researcher-agent'; import { WORKFLOW_BUILDER_AGENT } from './agents/workflow-builder-agent'; import { type StandardAgentDefinition } from './types/standard-agent-definition.interface'; -export const standardAgentDefinitions = [ +export const STANDARD_AGENT_DEFINITIONS = [ WORKFLOW_BUILDER_AGENT, DATA_MANIPULATOR_AGENT, DASHBOARD_BUILDER_AGENT, HELPER_AGENT, RESEARCHER_AGENT, - // CODE_AGENT, + METADATA_BUILDER_AGENT, ] as const satisfies StandardAgentDefinition[]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-model-manager-role.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-model-manager-role.ts new file mode 100644 index 0000000000..f38e4c2fc4 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-model-manager-role.ts @@ -0,0 +1,21 @@ +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; +import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface'; + +export const DATA_MODEL_MANAGER_ROLE: StandardRoleDefinition = { + standardId: '20202020-0001-0001-0001-000000000006', + label: 'Data Model Manager', + description: 'Role for managing the workspace data model', + icon: 'IconDatabaseEdit', + isEditable: false, + canUpdateAllSettings: false, + canAccessAllTools: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canBeAssignedToUsers: false, + canBeAssignedToAgents: true, + canBeAssignedToApiKeys: false, + permissionFlags: [PermissionFlagType.DATA_MODEL], + applicationId: null, +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/standard-role-definitions.ts similarity index 77% rename from packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts rename to packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/standard-role-definitions.ts index 92d3b7feb6..72bc911254 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/standard-role-definitions.ts @@ -1,12 +1,14 @@ import { ADMIN_ROLE } from './roles/admin-role'; import { DASHBOARD_MANAGER_ROLE } from './roles/dashboard-manager-role'; import { DATA_MANIPULATOR_ROLE } from './roles/data-manipulator-role'; +import { DATA_MODEL_MANAGER_ROLE } from './roles/data-model-manager-role'; import { WORKFLOW_MANAGER_ROLE } from './roles/workflow-manager-role'; import { type StandardRoleDefinition } from './types/standard-role-definition.interface'; -export const standardRoleDefinitions = [ +export const STANDARD_ROLE_DEFINITIONS = [ ADMIN_ROLE, WORKFLOW_MANAGER_ROLE, DATA_MANIPULATOR_ROLE, DASHBOARD_MANAGER_ROLE, + DATA_MODEL_MANAGER_ROLE, ] as const satisfies StandardRoleDefinition[];