diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index ab4e353af8..b3ceb1b5ba 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -968,8 +968,10 @@ export type CreateServerlessFunctionInput = { description?: InputMaybe; handlerName?: InputMaybe; handlerPath?: InputMaybe; + isTool?: InputMaybe; name: Scalars['String']; timeoutSeconds?: InputMaybe; + toolInputSchema?: InputMaybe; }; export type CreateSkillInput = { @@ -3422,6 +3424,7 @@ export type Query = { getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal; getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; + getToolIndex: Array; index: Index; indexMetadatas: IndexConnection; listPlans: Array; @@ -4103,6 +4106,7 @@ export type Sentry = { export type ServerlessFunction = { __typename?: 'ServerlessFunction'; + applicationId?: Maybe; createdAt: Scalars['DateTime']; cronTriggers?: Maybe>; databaseEventTriggers?: Maybe>; @@ -4110,12 +4114,14 @@ export type ServerlessFunction = { handlerName: Scalars['String']; handlerPath: Scalars['String']; id: Scalars['UUID']; + isTool: Scalars['Boolean']; latestVersion?: Maybe; name: Scalars['String']; publishedVersions: Array; routeTriggers?: Maybe>; runtime: Scalars['String']; timeoutSeconds: Scalars['Float']; + toolInputSchema?: Maybe; updatedAt: Scalars['DateTime']; }; @@ -4413,6 +4419,15 @@ export type TimelineThreadsWithTotal = { totalNumberOfThreads: Scalars['Int']; }; +export type ToolIndexEntry = { + __typename?: 'ToolIndexEntry'; + category: Scalars['String']; + description: Scalars['String']; + inputSchema?: Maybe; + name: Scalars['String']; + objectName?: Maybe; +}; + export type TransientTokenOutput = { __typename?: 'TransientTokenOutput'; transientToken: AuthToken; @@ -4660,8 +4675,10 @@ export type UpdateServerlessFunctionInputUpdates = { description?: InputMaybe; handlerName?: InputMaybe; handlerPath?: InputMaybe; + isTool?: InputMaybe; name: Scalars['String']; timeoutSeconds?: InputMaybe; + toolInputSchema?: InputMaybe; }; export type UpdateSkillInput = { @@ -5427,6 +5444,11 @@ export type GetChatThreadsQueryVariables = Exact<{ [key: string]: never; }>; export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: Array<{ __typename?: 'AgentChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, contextWindowTokens?: number | null, totalInputCredits: number, totalOutputCredits: number, createdAt: string, updatedAt: string }> }; +export type GetToolIndexQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetToolIndexQuery = { __typename?: 'Query', getToolIndex: Array<{ __typename?: 'ToolIndexEntry', name: string, description: string, category: string, objectName?: string | null, inputSchema?: any | null }> }; + export type TrackAnalyticsMutationVariables = Exact<{ type: AnalyticsType; event?: InputMaybe; @@ -5446,7 +5468,7 @@ export type UpdateOneApplicationVariableMutationVariables = Exact<{ export type UpdateOneApplicationVariableMutation = { __typename?: 'Mutation', updateOneApplicationVariable: boolean }; -export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, applicationId?: string | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> }; +export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, applicationId?: string | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> }; export type FindManyApplicationsQueryVariables = Exact<{ [key: string]: never; }>; @@ -5458,7 +5480,7 @@ export type FindOneApplicationQueryVariables = Exact<{ }>; -export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, applicationId?: string | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> } }; +export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, applicationId?: string | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> } }; export type UploadFileMutationVariables = Exact<{ file: Scalars['Upload']; @@ -6306,21 +6328,21 @@ export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }; +export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }; export type CreateOneServerlessFunctionItemMutationVariables = Exact<{ input: CreateServerlessFunctionInput; }>; -export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; +export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; export type DeleteOneServerlessFunctionMutationVariables = Exact<{ input: ServerlessFunctionIdInput; }>; -export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; +export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; export type ExecuteOneServerlessFunctionMutationVariables = Exact<{ input: ExecuteServerlessFunctionInput; @@ -6334,14 +6356,14 @@ export type PublishOneServerlessFunctionMutationVariables = Exact<{ }>; -export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; +export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; export type UpdateOneServerlessFunctionMutationVariables = Exact<{ input: UpdateServerlessFunctionInput; }>; -export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; +export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; export type FindManyAvailablePackagesQueryVariables = Exact<{ input: ServerlessFunctionIdInput; @@ -6353,14 +6375,14 @@ export type FindManyAvailablePackagesQuery = { __typename?: 'Query', getAvailabl export type GetManyServerlessFunctionsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> }; +export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null }> }; export type GetOneServerlessFunctionQueryVariables = Exact<{ input: ServerlessFunctionIdInput; }>; -export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; +export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, createdAt: string, updatedAt: string }> | null } }; export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{ input: GetServerlessFunctionSourceCodeInput; @@ -7047,6 +7069,9 @@ export const ServerlessFunctionFieldsFragmentDoc = gql` createdAt updatedAt } + toolInputSchema + isTool + applicationId createdAt updatedAt } @@ -8312,6 +8337,44 @@ export function useGetChatThreadsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt export type GetChatThreadsQueryHookResult = ReturnType; export type GetChatThreadsLazyQueryHookResult = ReturnType; export type GetChatThreadsQueryResult = Apollo.QueryResult; +export const GetToolIndexDocument = gql` + query GetToolIndex { + getToolIndex { + name + description + category + objectName + inputSchema + } +} + `; + +/** + * __useGetToolIndexQuery__ + * + * To run a query within a React component, call `useGetToolIndexQuery` and pass it any options that fit your needs. + * When your component renders, `useGetToolIndexQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetToolIndexQuery({ + * variables: { + * }, + * }); + */ +export function useGetToolIndexQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetToolIndexDocument, options); + } +export function useGetToolIndexLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetToolIndexDocument, options); + } +export type GetToolIndexQueryHookResult = ReturnType; +export type GetToolIndexLazyQueryHookResult = ReturnType; +export type GetToolIndexQueryResult = Apollo.QueryResult; export const TrackAnalyticsDocument = gql` mutation TrackAnalytics($type: AnalyticsType!, $event: String, $name: String, $properties: JSON) { trackAnalytics(type: $type, event: $event, name: $name, properties: $properties) { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index a7005a36c2..2493da8c29 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -951,8 +951,10 @@ export type CreateServerlessFunctionInput = { description?: InputMaybe; handlerName?: InputMaybe; handlerPath?: InputMaybe; + isTool?: InputMaybe; name: Scalars['String']; timeoutSeconds?: InputMaybe; + toolInputSchema?: InputMaybe; }; export type CreateViewFieldInput = { @@ -3298,6 +3300,7 @@ export type Query = { getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal; getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; + getToolIndex: Array; index: Index; indexMetadatas: IndexConnection; listPlans: Array; @@ -3895,6 +3898,7 @@ export type Sentry = { export type ServerlessFunction = { __typename?: 'ServerlessFunction'; + applicationId?: Maybe; createdAt: Scalars['DateTime']; cronTriggers?: Maybe>; databaseEventTriggers?: Maybe>; @@ -3902,12 +3906,14 @@ export type ServerlessFunction = { handlerName: Scalars['String']; handlerPath: Scalars['String']; id: Scalars['UUID']; + isTool: Scalars['Boolean']; latestVersion?: Maybe; name: Scalars['String']; publishedVersions: Array; routeTriggers?: Maybe>; runtime: Scalars['String']; timeoutSeconds: Scalars['Float']; + toolInputSchema?: Maybe; updatedAt: Scalars['DateTime']; }; @@ -4205,6 +4211,15 @@ export type TimelineThreadsWithTotal = { totalNumberOfThreads: Scalars['Int']; }; +export type ToolIndexEntry = { + __typename?: 'ToolIndexEntry'; + category: Scalars['String']; + description: Scalars['String']; + inputSchema?: Maybe; + name: Scalars['String']; + objectName?: Maybe; +}; + export type TransientTokenOutput = { __typename?: 'TransientTokenOutput'; transientToken: AuthToken; @@ -4444,8 +4459,10 @@ export type UpdateServerlessFunctionInputUpdates = { description?: InputMaybe; handlerName?: InputMaybe; handlerPath?: InputMaybe; + isTool?: InputMaybe; name: Scalars['String']; timeoutSeconds?: InputMaybe; + toolInputSchema?: InputMaybe; }; export type UpdateViewFieldInput = { diff --git a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx index f135d9f409..538ca49f94 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx @@ -57,6 +57,40 @@ const InitialLoadingIndicator = () => { ); }; +const MessagePartRenderer = ({ part }: { part: ExtendedUIMessagePart }) => { + switch (part.type) { + case 'reasoning': + return ( + + ); + case 'text': + return ; + case 'data-routing-status': + return ; + case 'data-code-execution': + return ( + + ); + default: + if (isToolUIPart(part)) { + return ; + } + return null; + } +}; + export const AIChatAssistantMessageRenderer = ({ messageParts, isLastMessageStreaming, @@ -75,44 +109,6 @@ export const AIChatAssistantMessageRenderer = ({ ? messageParts.filter((part) => part.type !== 'data-code-execution') : messageParts; - const renderMessagePart = (part: ExtendedUIMessagePart, index: number) => { - switch (part.type) { - case 'reasoning': - return ( - - ); - case 'text': - return ; - case 'data-routing-status': - return ; - case 'data-code-execution': - return ( - - ); - default: - { - if (isToolUIPart(part)) { - return ; - } - } - return null; - } - }; - if (!filteredParts.length && !hasError) { return ; } @@ -120,7 +116,9 @@ export const AIChatAssistantMessageRenderer = ({ return (
- {filteredParts.map(renderMessagePart)} + {filteredParts.map((part, index) => ( + + ))} {isLastMessageStreaming && !hasError && }
diff --git a/packages/twenty-front/src/modules/ai/graphql/queries/getToolIndex.ts b/packages/twenty-front/src/modules/ai/graphql/queries/getToolIndex.ts new file mode 100644 index 0000000000..2002c30325 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/queries/getToolIndex.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; + +export const GET_TOOL_INDEX = gql` + query GetToolIndex { + getToolIndex { + name + description + category + objectName + inputSchema + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/hooks/useGetToolIndex.ts b/packages/twenty-front/src/modules/ai/hooks/useGetToolIndex.ts new file mode 100644 index 0000000000..263aafe4b9 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/hooks/useGetToolIndex.ts @@ -0,0 +1,28 @@ +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { GET_TOOL_INDEX } from '@/ai/graphql/queries/getToolIndex'; +import { useQuery } from '@apollo/client'; + +type ToolIndexEntry = { + name: string; + description: string; + category: string; + objectName?: string; +}; + +type GetToolIndexQuery = { + getToolIndex: ToolIndexEntry[]; +}; + +export const useGetToolIndex = () => { + const apolloMetadataClient = useApolloCoreClient(); + + const { data, loading, error } = useQuery(GET_TOOL_INDEX, { + client: apolloMetadataClient ?? undefined, + }); + + return { + toolIndex: data?.getToolIndex ?? [], + loading, + error, + }; +}; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index cadf50811d..5e0d53069e 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -454,6 +454,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( path={SettingsPath.AISkillDetail} element={} /> + } + /> } /> } /> => { + const { getFunctionInputSchema } = await import('./getFunctionInputSchema'); + const inputSchema = getFunctionInputSchema(sourceCode); + + // Serverless functions take a single params object + const firstParam = inputSchema[0]; + + if (firstParam?.type === 'object' && isDefined(firstParam.properties)) { + return { + type: 'object', + properties: firstParam.properties, + }; + } + + return null; +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionLabelContainer.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionLabelContainer.tsx new file mode 100644 index 0000000000..d199c710df --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionLabelContainer.tsx @@ -0,0 +1,36 @@ +import { TitleInput } from '@/ui/input/components/TitleInput'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; + +const StyledHeaderTitle = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + font-size: ${({ theme }) => theme.font.size.lg}; + width: fit-content; + max-width: 420px; + & > input:disabled { + color: ${({ theme }) => theme.font.color.primary}; + } +`; + +type SettingsServerlessFunctionLabelContainerProps = { + value: string; + onChange: (value: string) => void; +}; + +export const SettingsServerlessFunctionLabelContainer = ({ + value, + onChange, +}: SettingsServerlessFunctionLabelContainerProps) => { + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx index ce1164293a..e2b1071cd7 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx @@ -21,11 +21,13 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ handleExecute, onChange, isTesting = false, + isManaged = false, }: { files: File[]; handleExecute: () => void; onChange: (filePath: string, value: string) => void; isTesting?: boolean; + isManaged?: boolean; }) => { const activeTabId = useRecoilComponentValue( activeTabIdComponentState, @@ -64,12 +66,16 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ files={files} currentFilePath={activeTabId} onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)} - options={{ - readOnly: true, - readOnlyMessage: { - value: t`Managed serverless functions are not editable`, - }, - }} + options={ + isManaged + ? { + readOnly: true, + readOnlyMessage: { + value: t`Managed serverless functions are not editable`, + }, + } + : undefined + } /> )} diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx index c3c0960dff..b83954558c 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx @@ -1,6 +1,6 @@ import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm'; -import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/SettingsServerlessFunctionTabEnvironmentVariablesSection'; +import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; export const SettingsServerlessFunctionSettingsTab = ({ formValues, @@ -14,7 +14,6 @@ export const SettingsServerlessFunctionSettingsTab = ({ diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTriggersTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTriggersTab.tsx index b51ba85640..8724a25386 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTriggersTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTriggersTab.tsx @@ -26,6 +26,16 @@ const StyledRouteTriggerTableHeaderRow = styled(StyledRouteTriggerTableRow)` margin-bottom: ${({ theme }) => theme.spacing(2)}; `; +const StyledEmptyState = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + font-size: ${({ theme }) => theme.font.size.md}; + height: 160px; + justify-content: center; + text-align: center; +`; + export const SettingsServerlessFunctionTriggersTab = ({ serverlessFunction, }: { @@ -46,6 +56,25 @@ export const SettingsServerlessFunctionTriggersTab = ({ return { object, action }; }); + const hasNoTriggers = + databaseEvents.length === 0 && + cronTriggers.length === 0 && + routeTriggers.length === 0; + + if (hasNoTriggers) { + return ( +
+ + + {t`No triggers configured for this function.`} + +
+ ); + } + return ( <> {databaseEvents.length > 0 && ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts index de6354440c..d81ce6d219 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts @@ -31,6 +31,9 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql` createdAt updatedAt } + toolInputSchema + isTool + applicationId createdAt updatedAt } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts index 3b61904fef..5f62d0841b 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts @@ -1,15 +1,18 @@ -import { useQuery } from '@apollo/client'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction'; +import { useQuery } from '@apollo/client'; import { - type ServerlessFunctionIdInput, type GetOneServerlessFunctionQuery, type GetOneServerlessFunctionQueryVariables, + type ServerlessFunctionIdInput, } from '~/generated-metadata/graphql'; -export const useGetOneServerlessFunction = ( - input: ServerlessFunctionIdInput, -) => { +export const useGetOneServerlessFunction = ({ + id, + onCompleted, +}: ServerlessFunctionIdInput & { + onCompleted?: (data: GetOneServerlessFunctionQuery) => void; +}) => { const apolloMetadataClient = useApolloCoreClient(); const { data, loading } = useQuery< GetOneServerlessFunctionQuery, @@ -17,8 +20,9 @@ export const useGetOneServerlessFunction = ( >(FIND_ONE_SERVERLESS_FUNCTION, { client: apolloMetadataClient ?? undefined, variables: { - input, + input: { id }, }, + onCompleted, }); return { serverlessFunction: data?.findOneServerlessFunction || null, diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts index 358c0f4404..70eef3ba93 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts @@ -1,14 +1,17 @@ +import { flattenSources } from '@/serverless-functions/utils/flattenSources'; import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode'; import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction'; import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode'; import { serverlessFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/serverlessFunctionTestDataFamilyState'; import { type Dispatch, type SetStateAction, useState } from 'react'; import { useRecoilState } from 'recoil'; -import { type FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql'; -import { type ServerlessFunction } from '~/generated/graphql'; import { type Sources } from 'twenty-shared/types'; -import { flattenSources } from '@/serverless-functions/utils/flattenSources'; import { isDefined } from 'twenty-shared/utils'; +import { + type FindOneServerlessFunctionSourceCodeQuery, + type GetOneServerlessFunctionQuery, +} from '~/generated-metadata/graphql'; +import { type ServerlessFunction } from '~/generated/graphql'; export type ServerlessFunctionNewFormValues = { name: string; @@ -47,6 +50,17 @@ export const useServerlessFunctionUpdateFormState = ({ const { serverlessFunction, loading: serverlessFunctionLoading } = useGetOneServerlessFunction({ id: serverlessFunctionId, + onCompleted: (data: GetOneServerlessFunctionQuery) => { + const fn = data?.findOneServerlessFunction; + + if (isDefined(fn)) { + setFormValues((prevState) => ({ + ...prevState, + name: fn.name || '', + description: fn.description || '', + })); + } + }, }); const { loading: serverlessFunctionSourceCodeLoading } = @@ -56,15 +70,9 @@ export const useServerlessFunctionUpdateFormState = ({ onCompleted: async (data: FindOneServerlessFunctionSourceCodeQuery) => { const code = data?.getServerlessFunctionSourceCode; - const newState = { - code: code || undefined, - name: serverlessFunction?.name || '', - description: serverlessFunction?.description || '', - }; - setFormValues((prevState) => ({ ...prevState, - ...newState, + code: code || prevState.code, })); if (serverlessFunctionTestData.shouldInitInput) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useWorkflowAiAgentPermissionActions.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useWorkflowAiAgentPermissionActions.ts index fad69af665..e951a0a589 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useWorkflowAiAgentPermissionActions.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useWorkflowAiAgentPermissionActions.ts @@ -116,7 +116,13 @@ export const useWorkflowAiAgentPermissionActions = ({ }, }); - await refetchAgentAndRoles(); + const { refetchedAgent } = await refetchAgentAndRoles(); + + // Update Recoil state with the refetched agent to ensure roleId is available + // Apollo's onCompleted is not called on refetch, so we need to update manually + if (isDefined(refetchedAgent)) { + setWorkflowAiAgentActionAgent(refetchedAgent); + } return generatedRoleId; }; @@ -294,7 +300,6 @@ export const useWorkflowAiAgentPermissionActions = ({ return; } - const hadRoleBefore = isDefined(roleId); const ensuredRoleId = await ensureRoleId(); if (!ensuredRoleId || permissionFlagKeys.includes(permissionFlagKey)) { @@ -310,14 +315,7 @@ export const useWorkflowAiAgentPermissionActions = ({ }, }); - const { refetchedAgent } = await refetchAgentAndRoles(); - if ( - !hadRoleBefore && - isDefined(refetchedAgent) && - isDefined(setWorkflowAiAgentActionAgent) - ) { - setWorkflowAiAgentActionAgent(refetchedAgent); - } + await refetchAgentAndRoles(); setWorkflowAiAgentPermissionsIsAddingPermission(false); setWorkflowAiAgentPermissionsSelectedObjectId(undefined); }; diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx index 35a15133a4..61913d3443 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx @@ -1,14 +1,47 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { TabList } from '@/ui/layout/tab-list/components/TabList'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { t } from '@lingui/core/macro'; +import { IconSettings, IconSparkles, IconTool } from 'twenty-ui/display'; import { SettingsAIMCP } from './components/SettingsAIMCP'; import { SettingsAIRouterSettings } from './components/SettingsAIRouterSettings'; import { SettingsSkillsTable } from './components/SettingsSkillsTable'; +import { SettingsToolsTable } from './components/SettingsToolsTable'; +import { SETTINGS_AI_TABS } from './constants/SettingsAiTabs'; export const SettingsAI = () => { + const activeTabId = useRecoilComponentValue( + activeTabIdComponentState, + SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID, + ); + + const tabs = [ + { + id: SETTINGS_AI_TABS.TABS_IDS.SKILLS, + title: t`Skills`, + Icon: IconSparkles, + }, + { + id: SETTINGS_AI_TABS.TABS_IDS.TOOLS, + title: t`Tools`, + Icon: IconTool, + }, + { + id: SETTINGS_AI_TABS.TABS_IDS.SETTINGS, + title: t`Settings`, + Icon: IconSettings, + }, + ]; + + const isSkillsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.SKILLS; + const isToolsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.TOOLS; + const isSettingsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.SETTINGS; + return ( { ]} > - - - + + {isSkillsTab && } + {isToolsTab && } + {isSettingsTab && ( + <> + + + + )} ); diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx index 427ec0a1d2..857028b8c0 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx @@ -367,52 +367,16 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { : agent?.label : t`New Agent`; - const renderActiveTabContent = () => { - switch (activeTabId) { - case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.ROLE: - return ( - - ); + const isRoleTab = activeTabId === SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.ROLE; + const isSettingsTab = + activeTabId === SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.SETTINGS; + const isEvalsTab = activeTabId === SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.EVALS; + const isLogsTab = activeTabId === SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.LOGS; - case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.SETTINGS: - return ( - - ); - - case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.EVALS: - return ( - - handleFieldChange('evaluationInputs', inputs) - } - disabled={ - process.env.NODE_ENV === 'development' - ? isReadonlyMode - : isReadonlyMode || (isEditMode ? !agent?.isCustom : false) - } - /> - ); - - case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.LOGS: - return ; - - default: - return <>; - } - }; + const isFormDisabled = + isReadonlyMode || (isEditMode ? !agent?.isCustom : false); + const isEvalsDisabled = + process.env.NODE_ENV === 'development' ? isReadonlyMode : isFormDisabled; return ( <> @@ -451,7 +415,34 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { componentInstanceId={tabListComponentId} /> - {renderActiveTabContent()} + {isRoleTab && ( + + )} + {isSettingsTab && ( + + )} + {isEvalsTab && ( + + handleFieldChange('evaluationInputs', inputs) + } + disabled={isEvalsDisabled} + /> + )} + {isLogsTab && } )} diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsSkillForm.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsSkillForm.tsx index 93762d74a3..a744571e1c 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsSkillForm.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsSkillForm.tsx @@ -392,32 +392,26 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => { ? t`Deactivate "Synchronize Label and API Name" to set a custom API name` : t`Input must be in camel case and cannot start with a number`; - const renderTitle = () => { - if (isCreateMode) { - return t`New Skill`; - } - - if (loading === true) { - return t`Skill`; - } - - return ( - - handleFieldChange('label', value)} - placeholder={t`Skill name`} - /> - - ); - }; + const title = isCreateMode ? ( + t`New Skill` + ) : loading ? ( + t`Skill` + ) : ( + + handleFieldChange('label', value)} + placeholder={t`Skill name`} + /> + + ); return ( theme.spacing(2)}; - margin-bottom: ${({ theme }) => theme.spacing(2)}; - width: 100%; + align-items: center; + padding-bottom: ${({ theme }) => theme.spacing(2)}; `; const StyledSearchInput = styled(SettingsTextInput)` flex: 1; -`; - -const StyledTable = styled(Table)` - margin-top: ${({ theme }) => theme.spacing(3)}; + width: 100%; `; const StyledTableHeaderRow = styled(StyledSkillTableRow)` margin-bottom: ${({ theme }) => theme.spacing(2)}; `; -const StyledHeaderContainer = styled.div` +const StyledFooterContainer = styled.div` align-items: center; display: flex; - justify-content: space-between; + justify-content: flex-end; + margin-top: ${({ theme }) => theme.spacing(4)}; `; export const SettingsSkillsTable = () => { @@ -124,23 +120,8 @@ export const SettingsSkillsTable = () => { }; return ( -
- - - -
+ +
+ + + + + + + {t`Name`} + {t`Type`} + + + {filteredSystemTools.map((systemTool) => ( + + ))} +
+
+ + ); +}; diff --git a/packages/twenty-front/src/pages/settings/ai/constants/SettingsAiTabs.ts b/packages/twenty-front/src/pages/settings/ai/constants/SettingsAiTabs.ts new file mode 100644 index 0000000000..02a027c174 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/ai/constants/SettingsAiTabs.ts @@ -0,0 +1,8 @@ +export const SETTINGS_AI_TABS = { + COMPONENT_INSTANCE_ID: 'settings-ai-tab-list', + TABS_IDS: { + SKILLS: 'skills', + TOOLS: 'tools', + SETTINGS: 'settings', + }, +} as const; diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx index 0869346e48..780612199d 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -1,7 +1,10 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useTestServerlessFunction } from '@/serverless-functions/hooks/useTestServerlessFunction'; import { computeNewSources } from '@/serverless-functions/utils/computeNewSources'; import { flattenSources } from '@/serverless-functions/utils/flattenSources'; +import { getToolInputSchemaFromSourceCode } from '@/serverless-functions/utils/getToolInputSchemaFromSourceCode'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SettingsServerlessFunctionLabelContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionLabelContainer'; import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab'; import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab'; import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab'; @@ -14,8 +17,9 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { useNavigate, useParams } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { IconBolt, IconCode, @@ -31,6 +35,7 @@ export const SettingsServerlessFunctionDetail = () => { const { serverlessFunctionId = '', applicationId = '' } = useParams(); const navigate = useNavigate(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const { data } = useFindOneApplicationQuery({ variables: { id: applicationId }, @@ -39,6 +44,15 @@ export const SettingsServerlessFunctionDetail = () => { const applicationName = data?.findOneApplication?.name; + // A serverless function is "managed" if it belongs to an application + // other than the workspace's custom application + const workspaceCustomApplicationId = + currentWorkspace?.workspaceCustomApplication?.id; + const isManaged = + isDefined(applicationId) && + applicationId !== '' && + applicationId !== workspaceCustomApplicationId; + const instanceId = `${SERVERLESS_FUNCTION_DETAIL_ID}-${serverlessFunctionId}`; const activeTabId = useRecoilComponentValue( @@ -54,18 +68,22 @@ export const SettingsServerlessFunctionDetail = () => { serverlessFunctionId, }); - const handleSave = useDebouncedCallback(async () => { - await updateServerlessFunction({ - input: { - id: serverlessFunctionId, - update: { - name: formValues.name, - description: formValues.description, - code: formValues.code, + const handleSave = useDebouncedCallback( + async (toolInputSchema?: object | null) => { + await updateServerlessFunction({ + input: { + id: serverlessFunctionId, + update: { + name: formValues.name, + description: formValues.description, + code: formValues.code, + ...(toolInputSchema !== undefined && { toolInputSchema }), + }, }, - }, - }); - }, 500); + }); + }, + 500, + ); const onChange = (key: string) => { return async (value: string) => { @@ -88,7 +106,15 @@ export const SettingsServerlessFunctionDetail = () => { }), }; }); - await handleSave(); + + // Parse and save schema if editing the handler file + let toolInputSchema: object | null | undefined; + + if (filePath === serverlessFunction?.handlerPath) { + toolInputSchema = await getToolInputSchemaFromSourceCode(value); + } + + await handleSave(toolInputSchema); }; const handleTestFunction = async () => { @@ -119,48 +145,14 @@ export const SettingsServerlessFunctionDetail = () => { : 0, ); - const renderActiveTabContent = () => { - switch (activeTabId) { - case 'editor': - return ( - - ); - case 'triggers': - return serverlessFunction ? ( - - ) : null; - case 'test': - return ( - - ); - case 'settings': - return ( - - ); - default: - return <>; - } - }; + const isEditorTab = activeTabId === 'editor'; + const isTriggersTab = activeTabId === 'triggers'; + const isTestTab = activeTabId === 'test'; + const isSettingsTab = activeTabId === 'settings'; - return ( - !loading && ( - { ), }, { children: `${serverlessFunction?.name}` }, - ]} + ] + : [ + { + children: t`Workspace`, + href: getSettingsPath(SettingsPath.Workspace), + }, + { + children: t`AI`, + href: getSettingsPath(SettingsPath.AI), + }, + { children: `${serverlessFunction?.name}` }, + ]; + + return ( + !loading && ( + + } + links={breadcrumbLinks} > - {renderActiveTabContent()} + {isEditorTab && ( + + )} + {isTriggersTab && serverlessFunction && ( + + )} + {isTestTab && ( + + )} + {isSettingsTab && ( + + )} ) diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1767364430164-add-tool-schema-to-serverless-function.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1767364430164-add-tool-schema-to-serverless-function.ts new file mode 100644 index 0000000000..700c851414 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1767364430164-add-tool-schema-to-serverless-function.ts @@ -0,0 +1,25 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddToolSchemaToServerlessFunction1767364430164 + implements MigrationInterface +{ + name = 'AddToolSchemaToServerlessFunction1767364430164'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" ADD "toolInputSchema" jsonb`, + ); + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" ADD "isTool" boolean NOT NULL DEFAULT false`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" DROP COLUMN "isTool"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" DROP COLUMN "toolInputSchema"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts b/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts index e6d05a6747..0dfcf063bb 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts @@ -11,14 +11,14 @@ import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entit import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service'; +import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; describe('McpProtocolService', () => { let service: McpProtocolService; let featureFlagService: jest.Mocked; - let toolProviderService: jest.Mocked; + let toolRegistryService: jest.Mocked; let userRoleService: jest.Mocked; let mcpToolExecutorService: jest.Mocked; let apiKeyRoleService: jest.Mocked; @@ -37,8 +37,8 @@ describe('McpProtocolService', () => { isFeatureEnabled: jest.fn(), }; - const mockToolProviderService = { - getTools: jest.fn(), + const mockToolRegistryService = { + getToolsByCategories: jest.fn(), }; const mockUserRoleService = { @@ -62,8 +62,8 @@ describe('McpProtocolService', () => { useValue: mockFeatureFlagService, }, { - provide: ToolProviderService, - useValue: mockToolProviderService, + provide: ToolRegistryService, + useValue: mockToolRegistryService, }, { provide: UserRoleService, @@ -82,7 +82,7 @@ describe('McpProtocolService', () => { service = module.get(McpProtocolService); featureFlagService = module.get(FeatureFlagService); - toolProviderService = module.get(ToolProviderService); + toolRegistryService = module.get(ToolRegistryService); userRoleService = module.get(UserRoleService); mcpToolExecutorService = module.get(McpToolExecutorService); apiKeyRoleService = module.get(ApiKeyRoleService); @@ -227,7 +227,7 @@ describe('McpProtocolService', () => { testTool: mockTool, }; - toolProviderService.getTools.mockResolvedValue(mockToolsMap); + toolRegistryService.getToolsByCategories.mockResolvedValue(mockToolsMap); const mockToolCallResponse = { id: '123', @@ -282,7 +282,7 @@ describe('McpProtocolService', () => { testTool: mockTool, }; - toolProviderService.getTools.mockResolvedValue(mockToolsMap); + toolRegistryService.getToolsByCategories.mockResolvedValue(mockToolsMap); const mockToolCallResponse = { id: '123', @@ -316,7 +316,7 @@ describe('McpProtocolService', () => { }); expect(result).toEqual(mockToolCallResponse); - expect(toolProviderService.getTools).toHaveBeenCalled(); + expect(toolRegistryService.getToolsByCategories).toHaveBeenCalled(); }); it('should handle tools listing', async () => { @@ -330,7 +330,7 @@ describe('McpProtocolService', () => { }, }; - toolProviderService.getTools.mockResolvedValue(mockToolsMap); + toolRegistryService.getToolsByCategories.mockResolvedValue(mockToolsMap); const mockToolsListingResponse = { id: '123', @@ -398,7 +398,7 @@ describe('McpProtocolService', () => { it('should handle error when tool is not found', async () => { featureFlagService.isFeatureEnabled.mockResolvedValue(true); userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId); - toolProviderService.getTools.mockResolvedValue({}); + toolRegistryService.getToolsByCategories.mockResolvedValue({}); mcpToolExecutorService.handleToolCall.mockRejectedValue( new HttpException( diff --git a/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts b/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts index 6ee0f8477c..a464b5e46c 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts @@ -9,14 +9,14 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; -import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service'; +import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @Injectable() export class MCPMetadataService { constructor( private readonly featureFlagService: FeatureFlagService, - private readonly toolProvider: ToolProviderService, + private readonly toolRegistry: ToolRegistryService, private readonly metricsService: MetricsService, ) {} @@ -50,11 +50,18 @@ export class MCPMetadataService { } async getTools(workspaceId: string): Promise { - return this.toolProvider.getTools({ - workspaceId, - categories: [ToolCategory.METADATA], - wrapWithErrorContext: false, - }); + // Metadata tools don't require role-based permissions, using empty roleId + return this.toolRegistry.getToolsByCategories( + { + workspaceId, + roleId: '', + rolePermissionConfig: { unionOf: [] }, + }, + { + categories: [ToolCategory.METADATA], + wrapWithErrorContext: false, + }, + ); } async handleToolCall( diff --git a/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts b/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts index 8332995c7c..0704e8b7bf 100644 --- a/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts +++ b/packages/twenty-server/src/engine/api/mcp/services/mcp-protocol.service.ts @@ -12,7 +12,7 @@ import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api- import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; -import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service'; +import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; @@ -21,7 +21,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role export class McpProtocolService { constructor( private readonly featureFlagService: FeatureFlagService, - private readonly toolProvider: ToolProviderService, + private readonly toolRegistry: ToolRegistryService, private readonly userRoleService: UserRoleService, private readonly mcpToolExecutorService: McpToolExecutorService, private readonly apiKeyRoleService: ApiKeyRoleService, @@ -143,16 +143,22 @@ export class McpProtocolService { apiKey, ); - const toolSet = await this.toolProvider.getTools({ - workspaceId: workspace.id, - categories: [ToolCategory.DATABASE_CRUD, ToolCategory.ACTION], - rolePermissionConfig: { unionOf: [roleId] }, - authContext, - wrapWithErrorContext: false, - // Exclude code_interpreter from MCP to prevent recursive execution attacks - // (code running in the sandbox could call code_interpreter via MCP) - excludeTools: [ToolType.CODE_INTERPRETER], - }); + // Exclude code_interpreter from MCP to prevent recursive execution attacks + // (code running in the sandbox could call code_interpreter via MCP) + const toolSet = await this.toolRegistry.getToolsByCategories( + { + workspaceId: workspace.id, + roleId, + rolePermissionConfig: { unionOf: [roleId] }, + authContext, + userWorkspaceId, + }, + { + categories: [ToolCategory.DATABASE_CRUD, ToolCategory.ACTION], + excludeTools: [ToolType.CODE_INTERPRETER], + wrapWithErrorContext: false, + }, + ); if (method === 'tools/call' && params) { return await this.mcpToolExecutorService.handleToolCall( diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index 883b0f0c44..99de7c898d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -703,6 +703,8 @@ export class ApplicationSyncService { timeoutSeconds: serverlessFunctionToSync.timeoutSeconds, handlerPath: serverlessFunctionToSync.handlerPath, handlerName: serverlessFunctionToSync.handlerName, + toolInputSchema: serverlessFunctionToSync.toolInputSchema, + isTool: serverlessFunctionToSync.isTool, }, }; @@ -747,6 +749,8 @@ export class ApplicationSyncService { handlerName: serverlessFunctionToCreate.handlerName, applicationId, serverlessFunctionLayerId, + toolInputSchema: serverlessFunctionToCreate.toolInputSchema, + isTool: serverlessFunctionToCreate.isTool, }; const createdServerlessFunction = diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory.ts b/packages/twenty-server/src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory.ts index 11b8a037db..4cf640f7b0 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory.ts @@ -52,7 +52,13 @@ export const createDirectRecordToolsFactory = (deps: DirectRecordToolsDeps) => { restrictedFields, ), execute: async (parameters) => { - const { limit, offset, orderBy, ...filter } = parameters.input; + const { + loadingMessage: _, + limit, + offset, + orderBy, + ...filter + } = parameters; return deps.findRecordsService.execute({ objectName: objectMetadata.nameSingular, @@ -72,7 +78,7 @@ export const createDirectRecordToolsFactory = (deps: DirectRecordToolsDeps) => { execute: async (parameters) => { return deps.findRecordsService.execute({ objectName: objectMetadata.nameSingular, - filter: { id: { eq: parameters.input.id } }, + filter: { id: { eq: parameters.id } }, limit: 1, authContext, rolePermissionConfig: context.rolePermissionConfig, @@ -89,9 +95,11 @@ export const createDirectRecordToolsFactory = (deps: DirectRecordToolsDeps) => { restrictedFields, ), execute: async (parameters) => { + const { loadingMessage: _, ...objectRecord } = parameters; + return deps.createRecordService.execute({ objectName: objectMetadata.nameSingular, - objectRecord: parameters.input, + objectRecord, authContext, rolePermissionConfig: context.rolePermissionConfig, createdBy: context.actorContext, @@ -108,7 +116,7 @@ export const createDirectRecordToolsFactory = (deps: DirectRecordToolsDeps) => { restrictedFields, ), execute: async (parameters) => { - const { id, ...allFields } = parameters.input; + const { loadingMessage: _, id, ...allFields } = parameters; const objectRecord = Object.fromEntries( Object.entries(allFields).filter( @@ -134,7 +142,7 @@ export const createDirectRecordToolsFactory = (deps: DirectRecordToolsDeps) => { execute: async (parameters) => { return deps.deleteRecordService.execute({ objectName: objectMetadata.nameSingular, - objectRecordId: parameters.input.id, + objectRecordId: parameters.id, authContext, rolePermissionConfig: context.rolePermissionConfig, soft: true, diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts index 2e38858564..a30a47a835 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts @@ -1,5 +1,4 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; -import { z } from 'zod'; import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema'; @@ -8,19 +7,9 @@ export const generateCreateRecordInputSchema = ( objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { - const recordPropertiesSchema = generateRecordPropertiesZodSchema( + return generateRecordPropertiesZodSchema( objectMetadata, false, restrictedFields, ); - - return z.object({ - loadingMessage: z - .string() - .optional() - .describe( - 'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.', - ), - input: recordPropertiesSchema, - }); }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts index cd99748400..4d08095da4 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts @@ -14,20 +14,10 @@ export const generateUpdateRecordInputSchema = ( restrictedFields, ); - const updateSchema = recordPropertiesSchema.partial().extend({ + return recordPropertiesSchema.partial().extend({ id: z.string().uuid({ message: 'The unique identifier (UUID) of the record to update. This is required to identify which record should be modified.', }), }); - - return z.object({ - loadingMessage: z - .string() - .optional() - .describe( - 'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.', - ), - input: updateSchema, - }); }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/bulk-delete-tool.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/bulk-delete-tool.zod-schema.ts index c551f63e84..c3173051a7 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/bulk-delete-tool.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/bulk-delete-tool.zod-schema.ts @@ -1,25 +1,17 @@ import { z } from 'zod'; export const BulkDeleteToolInputSchema = z.object({ - loadingMessage: z - .string() - .optional() - .describe( - 'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.', - ), - input: z.object({ - filter: z - .object({ - id: z - .object({ - in: z - .array(z.string().uuid()) - .describe('Array of record IDs to delete'), - }) - .describe('Filter to select records to delete'), - }) - .describe('Filter criteria to select records for bulk delete'), - }), + filter: z + .object({ + id: z + .object({ + in: z + .array(z.string().uuid()) + .describe('Array of record IDs to delete'), + }) + .describe('Filter to select records to delete'), + }) + .describe('Filter criteria to select records for bulk delete'), }); export type BulkDeleteToolInput = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema.ts index 2bfc870ab8..4027a54b29 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema.ts @@ -1,15 +1,7 @@ import { z } from 'zod'; export const FindOneToolInputSchema = z.object({ - loadingMessage: z - .string() - .optional() - .describe( - 'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.', - ), - input: z.object({ - id: z.string().uuid().describe('The unique UUID of the record to retrieve'), - }), + id: z.string().uuid().describe('The unique UUID of the record to retrieve'), }); export type FindOneToolInput = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts index fefbc8fcc3..1764342696 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts @@ -62,41 +62,33 @@ export const generateFindToolInputSchema = ( ); return z.object({ - loadingMessage: z - .string() + limit: z + .number() + .int() + .positive() + .max(1000) + .default(100) + .describe('Maximum number of records to return (default: 100)'), + offset: z + .number() + .int() + .nonnegative() + .default(0) + .describe('Number of records to skip (default: 0)'), + orderBy: ObjectRecordOrderBySchema.describe( + 'Sort records by field(s). CRITICAL for "top N", "largest", "smallest" queries. Each item is an object with exactly ONE property: field name as key, sort direction as value. Example: [{"employees": "DescNullsLast"}] sorts employees descending. Use "DescNullsLast" for top/largest, "AscNullsFirst" for bottom/smallest.', + ), + ...filterShape, + or: z + .array(filterSchema) .optional() - .describe( - 'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.', - ), - input: z.object({ - limit: z - .number() - .int() - .positive() - .max(1000) - .default(100) - .describe('Maximum number of records to return (default: 100)'), - offset: z - .number() - .int() - .nonnegative() - .default(0) - .describe('Number of records to skip (default: 0)'), - orderBy: ObjectRecordOrderBySchema.describe( - 'Sort records by field(s). CRITICAL for "top N", "largest", "smallest" queries. Each item is an object with exactly ONE property: field name as key, sort direction as value. Example: [{"employees": "DescNullsLast"}] sorts employees descending. Use "DescNullsLast" for top/largest, "AscNullsFirst" for bottom/smallest.', - ), - ...filterShape, - or: z - .array(filterSchema) - .optional() - .describe('OR condition - matches if ANY of the filters match'), - and: z - .array(filterSchema) - .optional() - .describe('AND condition - matches if ALL filters match'), - not: filterSchema - .optional() - .describe('NOT condition - matches if the filter does NOT match'), - }), + .describe('OR condition - matches if ANY of the filters match'), + and: z + .array(filterSchema) + .optional() + .describe('AND condition - matches if ALL filters match'), + not: filterSchema + .optional() + .describe('NOT condition - matches if the filter does NOT match'), }); }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema.ts index 9530c6ef93..36778b3b76 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema.ts @@ -1,18 +1,10 @@ import { z } from 'zod'; export const SoftDeleteToolInputSchema = z.object({ - loadingMessage: z + id: z .string() - .optional() - .describe( - 'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.', - ), - input: z.object({ - id: z - .string() - .uuid() - .describe('The unique UUID of the record to soft delete'), - }), + .uuid() + .describe('The unique UUID of the record to soft delete'), }); export type SoftDeleteToolInput = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/enums/tool-category.enum.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/enums/tool-category.enum.ts index 51d577a233..28b4931df4 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/enums/tool-category.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/enums/tool-category.enum.ts @@ -6,4 +6,5 @@ export enum ToolCategory { NATIVE_MODEL = 'NATIVE_MODEL', VIEW = 'VIEW', DASHBOARD = 'DASHBOARD', + SERVERLESS_FUNCTION = 'SERVERLESS_FUNCTION', } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts index 910b416a88..b7045307b1 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts @@ -2,21 +2,36 @@ import { type ToolSet } from 'ai'; import { type CodeExecutionData } from 'twenty-shared/ai'; import { type ActorMetadata } from 'twenty-shared/types'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; +import { type ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; +import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type CodeExecutionStreamEmitter = (data: CodeExecutionData) => void; +// Unified context for tool generation - used by all consumers export type ToolProviderContext = { workspaceId: string; roleId: string; rolePermissionConfig: RolePermissionConfig; + // Optional fields for different use cases + authContext?: WorkspaceAuthContext; actorContext?: ActorMetadata; userId?: string; userWorkspaceId?: string; + agent?: FlatAgentWithRoleId | null; onCodeExecutionUpdate?: CodeExecutionStreamEmitter; }; +// Options for tool retrieval +export type ToolRetrievalOptions = { + categories?: ToolCategory[]; + excludeTools?: ToolType[]; + wrapWithErrorContext?: boolean; +}; + export interface ToolProvider { readonly category: ToolCategory; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts index 7744eb8bdd..269e0dddf8 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { type ToolSet } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { type ZodObject, type ZodRawShape } from 'zod'; import { type ToolProvider, @@ -18,6 +19,10 @@ import { type Tool, type ToolExecutionContext, } from 'src/engine/core-modules/tool/types/tool.type'; +import { + stripLoadingMessage, + wrapSchemaForExecution, +} from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @Injectable() @@ -98,9 +103,11 @@ export class ActionToolProvider implements ToolProvider { private createToolEntry(tool: Tool, context: ToolExecutionContext) { return { description: tool.description, - inputSchema: tool.inputSchema, - execute: async (parameters: { input: ToolInput }) => - tool.execute(parameters.input, context), + inputSchema: wrapSchemaForExecution( + tool.inputSchema as ZodObject, + ), + execute: async (parameters: ToolInput) => + tool.execute(stripLoadingMessage(parameters), context), }; } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/native-model-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/native-model-tool.provider.ts new file mode 100644 index 0000000000..f0acea0d7a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/native-model-tool.provider.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; + +import { type ToolSet } from 'ai'; +import { isDefined } from 'twenty-shared/utils'; + +import { + type ToolProvider, + type ToolProviderContext, +} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; + +import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; +import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/agent-model-config.service'; +import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; + +@Injectable() +export class NativeModelToolProvider implements ToolProvider { + readonly category = ToolCategory.NATIVE_MODEL; + + constructor( + private readonly agentModelConfigService: AgentModelConfigService, + private readonly aiModelRegistryService: AiModelRegistryService, + ) {} + + async isAvailable(context: ToolProviderContext): Promise { + return isDefined(context.agent); + } + + async generateTools(context: ToolProviderContext): Promise { + if (!context.agent) { + return {}; + } + + const registeredModel = + await this.aiModelRegistryService.resolveModelForAgent(context.agent); + + return this.agentModelConfigService.getNativeModelTools( + registeredModel, + context.agent, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/serverless-function-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/serverless-function-tool.provider.ts new file mode 100644 index 0000000000..46e8cc0324 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/serverless-function-tool.provider.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@nestjs/common'; + +import { jsonSchema, type ToolSet } from 'ai'; +import { isDefined } from 'twenty-shared/utils'; + +import { + type ToolProvider, + type ToolProviderContext, +} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; + +import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; +import { wrapJsonSchemaForExecution } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; + +@Injectable() +export class ServerlessFunctionToolProvider implements ToolProvider { + readonly category = ToolCategory.SERVERLESS_FUNCTION; + + constructor( + private readonly serverlessFunctionService: ServerlessFunctionService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + ) {} + + async isAvailable(_context: ToolProviderContext): Promise { + // Serverless function tools are available if there are any functions marked as tools + return true; + } + + async generateTools(context: ToolProviderContext): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId: context.workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + // Filter serverless functions that are marked as tools + const serverlessFunctionsWithSchema = Object.values( + flatServerlessFunctionMaps.byId, + ).filter( + (fn): fn is FlatServerlessFunction => + isDefined(fn) && fn.isTool === true && fn.deletedAt === null, + ); + + const tools: ToolSet = {}; + + for (const serverlessFunction of serverlessFunctionsWithSchema) { + const toolName = this.buildServerlessFunctionToolName( + serverlessFunction.name, + ); + + const wrappedSchema = wrapJsonSchemaForExecution( + serverlessFunction.toolInputSchema as Record, + ); + + tools[toolName] = { + description: + serverlessFunction.description || + `Execute the ${serverlessFunction.name} serverless function`, + inputSchema: jsonSchema(wrappedSchema), + execute: async (parameters: Record) => { + const { loadingMessage: _, ...actualParams } = parameters; + + const result = + await this.serverlessFunctionService.executeOneServerlessFunction({ + id: serverlessFunction.id, + workspaceId: context.workspaceId, + payload: actualParams, + version: serverlessFunction.latestVersion ?? 'draft', + }); + + if (result.error) { + return { + success: false, + error: result.error.errorMessage, + }; + } + + return { + success: true, + result: result.data, + }; + }, + }; + } + + return tools; + } + + private buildServerlessFunctionToolName(functionName: string): string { + // Convert function name to a valid tool name (lowercase, underscores) + return `serverless_${functionName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, '')}`; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/resolvers/tool-index.resolver.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/resolvers/tool-index.resolver.ts new file mode 100644 index 0000000000..6884216e9d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/resolvers/tool-index.resolver.ts @@ -0,0 +1,59 @@ +import { UseGuards } from '@nestjs/common'; +import { Field, ObjectType, Query, Resolver } from '@nestjs/graphql'; + +import graphqlTypeJson from 'graphql-type-json'; + +import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; + +@ObjectType('ToolIndexEntry') +export class ToolIndexEntryDTO { + @Field() + name: string; + + @Field() + description: string; + + @Field() + category: string; + + @Field({ nullable: true }) + objectName?: string; + + @Field(() => graphqlTypeJson, { nullable: true }) + inputSchema?: object; +} + +@Resolver() +@UseGuards(WorkspaceAuthGuard) +export class ToolIndexResolver { + constructor( + private readonly toolRegistryService: ToolRegistryService, + private readonly userRoleService: UserRoleService, + ) {} + + @Query(() => [ToolIndexEntryDTO]) + @UseGuards(NoPermissionGuard) + async getToolIndex( + @AuthWorkspace() workspace: WorkspaceEntity, + @AuthUserWorkspaceId() userWorkspaceId: string, + ): Promise { + const roleId = await this.userRoleService.getRoleIdForUserWorkspace({ + userWorkspaceId, + workspaceId: workspace.id, + }); + + if (!roleId) { + return []; + } + + return this.toolRegistryService.buildToolIndex(workspace.id, roleId, { + userWorkspaceId, + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-provider.service.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-provider.service.ts deleted file mode 100644 index 4de5580e08..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-provider.service.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { Inject, Injectable, Logger, Optional } from '@nestjs/common'; - -import { type ToolSet } from 'ai'; -import { PermissionFlagType } from 'twenty-shared/constants'; - -import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service'; -import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service'; -import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service'; -import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service'; -import { createDirectRecordToolsFactory } from 'src/engine/core-modules/record-crud/tool-factory/direct-record-tools.factory'; -import { PerObjectToolGeneratorService } from 'src/engine/core-modules/tool-generator/services/per-object-tool-generator.service'; -import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token'; -import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; -import { type ToolSpecification } from 'src/engine/core-modules/tool-provider/types/tool-specification.type'; -import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; -import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool'; -import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool'; -import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool'; -import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool'; -import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; -import { type Tool } from 'src/engine/core-modules/tool/types/tool.type'; -import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/agent-model-config.service'; -import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; -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 { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -// Type-only import to avoid circular dependency at file level -import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service'; - -type ActionTool = { - tool: Tool; - flag?: PermissionFlagType; -}; - -@Injectable() -export class ToolProviderService { - private readonly logger = new Logger(ToolProviderService.name); - private readonly actionTools: Map; - - constructor( - private readonly httpTool: HttpTool, - private readonly sendEmailTool: SendEmailTool, - private readonly searchHelpCenterTool: SearchHelpCenterTool, - private readonly codeInterpreterTool: CodeInterpreterTool, - private readonly perObjectToolGenerator: PerObjectToolGeneratorService, - private readonly createRecordService: CreateRecordService, - private readonly updateRecordService: UpdateRecordService, - private readonly deleteRecordService: DeleteRecordService, - private readonly findRecordsService: FindRecordsService, - // Optional to avoid circular dependency with WorkflowExecutorModule (null when called from workflow context) - @Optional() - @Inject(WORKFLOW_TOOL_SERVICE_TOKEN) - private readonly workflowToolService: WorkflowToolWorkspaceService | null, - private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory, - private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory, - private readonly agentModelConfigService: AgentModelConfigService, - private readonly aiModelRegistryService: AiModelRegistryService, - private readonly permissionsService: PermissionsService, - ) { - this.actionTools = new Map([ - [ - ToolType.HTTP_REQUEST, - { - tool: this.httpTool, - flag: PermissionFlagType.HTTP_REQUEST_TOOL, - }, - ], - [ - ToolType.SEND_EMAIL, - { - tool: this.sendEmailTool, - flag: PermissionFlagType.SEND_EMAIL_TOOL, - }, - ], - [ - ToolType.SEARCH_HELP_CENTER, - { - tool: this.searchHelpCenterTool, - }, - ], - [ - ToolType.CODE_INTERPRETER, - { - tool: this.codeInterpreterTool, - flag: PermissionFlagType.CODE_INTERPRETER_TOOL, - }, - ], - ]); - } - - getToolByType(toolType: ToolType): Tool { - const actionTool = this.actionTools.get(toolType); - - if (!actionTool) { - throw new Error(`Unknown tool type: ${toolType}`); - } - - return actionTool.tool; - } - - async getTools(spec: ToolSpecification): Promise { - const tools: ToolSet = {}; - - for (const category of spec.categories) { - const categoryTools = await this.getToolsForCategory(category, spec); - - Object.assign(tools, categoryTools); - } - - this.logger.log( - `Generated ${Object.keys(tools).length} tools for categories: [${spec.categories.join(', ')}]`, - ); - - if (spec.wrapWithErrorContext) { - return this.wrapToolsWithErrorContext(tools); - } - - return tools; - } - - private async getToolsForCategory( - category: ToolCategory, - spec: ToolSpecification, - ): Promise { - switch (category) { - case ToolCategory.DATABASE_CRUD: - return this.getDatabaseTools(spec); - case ToolCategory.ACTION: - return this.getActionTools(spec); - case ToolCategory.WORKFLOW: - return this.getWorkflowTools(spec); - case ToolCategory.METADATA: - return this.getMetadataTools(spec); - case ToolCategory.NATIVE_MODEL: - return this.getNativeModelTools(spec); - default: - return {}; - } - } - - private async getDatabaseTools(spec: ToolSpecification): Promise { - if (!spec.rolePermissionConfig || !spec.authContext) { - return {}; - } - - const factory = createDirectRecordToolsFactory({ - createRecordService: this.createRecordService, - updateRecordService: this.updateRecordService, - deleteRecordService: this.deleteRecordService, - findRecordsService: this.findRecordsService, - }); - - return this.perObjectToolGenerator.generate( - { - workspaceId: spec.workspaceId, - authContext: spec.authContext, - rolePermissionConfig: spec.rolePermissionConfig, - actorContext: spec.actorContext, - }, - [factory], - ); - } - - private async getActionTools(spec: ToolSpecification): Promise { - const tools: ToolSet = {}; - const executionContext = { workspaceId: spec.workspaceId }; - const excludedTools = new Set(spec.excludeTools ?? []); - - for (const [toolType, { tool, flag }] of this.actionTools) { - if (excludedTools.has(toolType)) { - continue; - } - - if (!flag) { - tools[toolType.toLowerCase()] = { - description: tool.description, - inputSchema: tool.inputSchema, - execute: async (parameters: { input: ToolInput }) => - tool.execute(parameters.input, executionContext), - }; - } else if (spec.rolePermissionConfig && spec.workspaceId) { - const hasPermission = await this.permissionsService.hasToolPermission( - spec.rolePermissionConfig, - spec.workspaceId, - flag, - ); - - if (hasPermission) { - tools[toolType.toLowerCase()] = { - description: tool.description, - inputSchema: tool.inputSchema, - execute: async (parameters: { input: ToolInput }) => - tool.execute(parameters.input, executionContext), - }; - } - } - } - - return tools; - } - - private async getWorkflowTools(spec: ToolSpecification): Promise { - if (!this.workflowToolService) { - return {}; - } - - if (!spec.rolePermissionConfig) { - return {}; - } - - const hasWorkflowPermission = - await this.permissionsService.checkRolesPermissions( - spec.rolePermissionConfig, - spec.workspaceId, - PermissionFlagType.WORKFLOWS, - ); - - if (!hasWorkflowPermission) { - return {}; - } - - const workflowTools = this.workflowToolService.generateWorkflowTools( - spec.workspaceId, - spec.rolePermissionConfig, - ); - - const recordStepTools = - await this.workflowToolService.generateRecordStepConfiguratorTools( - spec.workspaceId, - spec.rolePermissionConfig, - ); - - return { ...workflowTools, ...recordStepTools }; - } - - private async getMetadataTools(spec: ToolSpecification): Promise { - if (spec.rolePermissionConfig) { - const hasDataModelPermission = - await this.permissionsService.checkRolesPermissions( - spec.rolePermissionConfig, - spec.workspaceId, - PermissionFlagType.DATA_MODEL, - ); - - if (!hasDataModelPermission) { - return {}; - } - } - - const objectMetadataTools = this.objectMetadataToolsFactory.generateTools( - spec.workspaceId, - ); - - const fieldMetadataTools = this.fieldMetadataToolsFactory.generateTools( - spec.workspaceId, - ); - - return { ...objectMetadataTools, ...fieldMetadataTools }; - } - - private async getNativeModelTools(spec: ToolSpecification): Promise { - if (!spec.agent) { - return {}; - } - - const registeredModel = - await this.aiModelRegistryService.resolveModelForAgent(spec.agent); - - return this.agentModelConfigService.getNativeModelTools( - registeredModel, - spec.agent, - ); - } - - private wrapToolsWithErrorContext(tools: ToolSet): ToolSet { - const wrappedTools: ToolSet = {}; - - for (const [toolName, tool] of Object.entries(tools)) { - if (!tool.execute) { - wrappedTools[toolName] = tool; - continue; - } - - const originalExecute = tool.execute; - - wrappedTools[toolName] = { - ...tool, - execute: async (...args: Parameters) => { - try { - return await originalExecute(...args); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - - return { - success: false, - error: { - message: errorMessage, - tool: toolName, - suggestion: this.generateErrorSuggestion( - toolName, - errorMessage, - ), - }, - }; - } - }, - }; - } - - return wrappedTools; - } - - private generateErrorSuggestion( - toolName: string, - errorMessage: string, - ): string { - const lowerError = errorMessage.toLowerCase(); - - if ( - lowerError.includes('not found') || - lowerError.includes('does not exist') - ) { - return 'Verify the ID or name exists with a search query first'; - } - - if ( - lowerError.includes('permission') || - lowerError.includes('forbidden') || - lowerError.includes('unauthorized') - ) { - return 'This operation requires elevated permissions or a different role'; - } - - if (lowerError.includes('invalid') || lowerError.includes('validation')) { - return 'Check the tool schema for valid parameter formats and types'; - } - - if ( - lowerError.includes('duplicate') || - lowerError.includes('already exists') - ) { - return 'A record with this identifier already exists. Try updating instead of creating'; - } - - if (lowerError.includes('required') || lowerError.includes('missing')) { - return 'Required fields are missing. Check which fields are mandatory for this operation'; - } - - return 'Try adjusting the parameters or using a different approach'; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts index f4bf5bae9d..c25b55612d 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts @@ -1,12 +1,14 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; -import { type ToolSet } from 'ai'; +import { type ToolSet, zodSchema } from 'ai'; import { type ActorMetadata } from 'twenty-shared/types'; +import { type ZodType } from 'zod'; import { type CodeExecutionStreamEmitter, type ToolProvider, type ToolProviderContext, + type ToolRetrievalOptions, } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token'; @@ -17,25 +19,28 @@ export type ToolIndexEntry = { name: string; description: string; category: - | 'database' - | 'action' - | 'workflow' - | 'metadata' - | 'view' - | 'dashboard'; + | 'DATABASE' + | 'ACTION' + | 'WORKFLOW' + | 'METADATA' + | 'VIEW' + | 'DASHBOARD' + | 'SERVERLESS_FUNCTION'; objectName?: string; operation?: string; + inputSchema?: object; }; export type ToolSearchOptions = { limit?: number; category?: - | 'database' - | 'action' - | 'workflow' - | 'metadata' - | 'view' - | 'dashboard'; + | 'DATABASE' + | 'ACTION' + | 'WORKFLOW' + | 'METADATA' + | 'VIEW' + | 'DASHBOARD' + | 'SERVERLESS_FUNCTION'; }; export type ToolContext = { @@ -183,6 +188,44 @@ export class ToolRegistryService { ); } + // Main method for eager loading tools by categories (replaces ToolProviderService.getTools) + async getToolsByCategories( + context: ToolProviderContext, + options: ToolRetrievalOptions = {}, + ): Promise { + const { categories, excludeTools, wrapWithErrorContext } = options; + const tools: ToolSet = {}; + + for (const provider of this.providers) { + if (categories && !categories.includes(provider.category)) { + continue; + } + if (await provider.isAvailable(context)) { + const providerTools = await provider.generateTools(context); + + Object.assign(tools, providerTools); + } + } + + // Apply excludeTools filter + if (excludeTools?.length) { + for (const toolType of excludeTools) { + delete tools[toolType.toLowerCase()]; + } + } + + this.logger.log( + `Generated ${Object.keys(tools).length} tools for categories: [${categories?.join(', ') ?? 'all'}]`, + ); + + // Apply error wrapping if requested + if (wrapWithErrorContext) { + return this.wrapToolsWithErrorContext(tools); + } + + return tools; + } + private buildContext( workspaceId: string, roleId: string, @@ -209,19 +252,176 @@ export class ToolRegistryService { category: ToolCategory, ): ToolIndexEntry[] { const categoryMap: Record = { - DATABASE_CRUD: 'database', - ACTION: 'action', - WORKFLOW: 'workflow', - METADATA: 'metadata', - NATIVE_MODEL: 'action', - VIEW: 'view', - DASHBOARD: 'dashboard', + DATABASE_CRUD: 'DATABASE', + ACTION: 'ACTION', + WORKFLOW: 'WORKFLOW', + METADATA: 'METADATA', + NATIVE_MODEL: 'ACTION', + VIEW: 'VIEW', + DASHBOARD: 'DASHBOARD', + SERVERLESS_FUNCTION: 'SERVERLESS_FUNCTION', }; - return Object.entries(tools).map(([name, tool]) => ({ - name, - description: tool.description ?? '', - category: categoryMap[category], - })); + return Object.entries(tools).map(([name, tool]) => { + const inputSchema = this.extractJsonSchema(tool.inputSchema); + + return { + name, + description: tool.description ?? '', + category: categoryMap[category], + inputSchema, + }; + }); + } + + private extractJsonSchema(inputSchema: unknown): object | undefined { + if (!inputSchema) { + return undefined; + } + + let schema: object | undefined; + + // Check if it's a Zod schema (has _def property) + if ( + typeof inputSchema === 'object' && + inputSchema !== null && + '_def' in inputSchema + ) { + try { + // Use AI SDK's zodSchema() to convert Zod to JSON Schema + const converted = zodSchema(inputSchema as ZodType); + + schema = converted.jsonSchema as object; + } catch { + // If conversion fails, return undefined + return undefined; + } + } else if ( + // Check if AI SDK wrapped it with jsonSchema property + typeof inputSchema === 'object' && + inputSchema !== null && + 'jsonSchema' in inputSchema + ) { + schema = (inputSchema as { jsonSchema: object }).jsonSchema; + } else if (typeof inputSchema === 'object') { + // Return as-is if it's already an object (plain JSON schema) + schema = inputSchema as object; + } + + if (!schema) { + return undefined; + } + + return this.stripInternalFieldsFromSchema(schema); + } + + // Remove internal fields (loadingMessage) from schema for display + private stripInternalFieldsFromSchema(schema: object): object { + const schemaObj = schema as Record; + + // Remove $schema property + const { $schema: _, ...rest } = schemaObj; + + // Remove loadingMessage from properties if present + // loadingMessage is an internal field auto-injected for AI status updates + if ( + rest.type === 'object' && + rest.properties && + typeof rest.properties === 'object' + ) { + const properties = rest.properties as Record; + const { loadingMessage: __, ...cleanProperties } = properties; + + // Filter required array to remove loadingMessage if present + const required = Array.isArray(rest.required) + ? rest.required.filter((field) => field !== 'loadingMessage') + : undefined; + + return { + ...rest, + properties: cleanProperties, + ...(required && required.length > 0 ? { required } : {}), + }; + } + + return rest; + } + + private wrapToolsWithErrorContext(tools: ToolSet): ToolSet { + const wrappedTools: ToolSet = {}; + + for (const [toolName, tool] of Object.entries(tools)) { + if (!tool.execute) { + wrappedTools[toolName] = tool; + continue; + } + + const originalExecute = tool.execute; + + wrappedTools[toolName] = { + ...tool, + execute: async (...args: Parameters) => { + try { + return await originalExecute(...args); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + return { + success: false, + error: { + message: errorMessage, + tool: toolName, + suggestion: this.generateErrorSuggestion( + toolName, + errorMessage, + ), + }, + }; + } + }, + }; + } + + return wrappedTools; + } + + private generateErrorSuggestion( + _toolName: string, + errorMessage: string, + ): string { + const lowerError = errorMessage.toLowerCase(); + + if ( + lowerError.includes('not found') || + lowerError.includes('does not exist') + ) { + return 'Verify the ID or name exists with a search query first'; + } + + if ( + lowerError.includes('permission') || + lowerError.includes('forbidden') || + lowerError.includes('unauthorized') + ) { + return 'This operation requires elevated permissions or a different role'; + } + + if (lowerError.includes('invalid') || lowerError.includes('validation')) { + return 'Check the tool schema for valid parameter formats and types'; + } + + if ( + lowerError.includes('duplicate') || + lowerError.includes('already exists') + ) { + return 'A record with this identifier already exists. Try updating instead of creating'; + } + + if (lowerError.includes('required') || lowerError.includes('missing')) { + return 'Required fields are missing. Check which fields are mandatory for this operation'; + } + + return 'Try adjusting the parameters or using a different approach'; } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts index 209c073150..dc3f43ae97 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts @@ -7,6 +7,8 @@ import { ActionToolProvider } from 'src/engine/core-modules/tool-provider/provid import { DashboardToolProvider } from 'src/engine/core-modules/tool-provider/providers/dashboard-tool.provider'; import { DatabaseToolProvider } from 'src/engine/core-modules/tool-provider/providers/database-tool.provider'; import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider'; +import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider'; +import { ServerlessFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/serverless-function-tool.provider'; import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider'; import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider'; import { ToolModule } from 'src/engine/core-modules/tool/tool.module'; @@ -16,10 +18,12 @@ import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/ 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 { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; +import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; -import { ToolProviderService } from './services/tool-provider.service'; +import { ToolIndexResolver } from './resolvers/tool-index.resolver'; import { ToolRegistryService } from './services/tool-registry.service'; // NOTE: This module does NOT import WorkflowToolsModule or DashboardToolsModule to avoid @@ -40,12 +44,17 @@ import { ToolRegistryService } from './services/tool-registry.service'; ViewModule, WorkspaceCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, + ServerlessFunctionModule, + UserRoleModule, ], providers: [ + ToolIndexResolver, ActionToolProvider, DashboardToolProvider, DatabaseToolProvider, MetadataToolProvider, + NativeModelToolProvider, + ServerlessFunctionToolProvider, ViewToolProvider, WorkflowToolProvider, { @@ -55,6 +64,8 @@ import { ToolRegistryService } from './services/tool-registry.service'; dashboardProvider: DashboardToolProvider, databaseProvider: DatabaseToolProvider, metadataProvider: MetadataToolProvider, + nativeModelProvider: NativeModelToolProvider, + serverlessFunctionProvider: ServerlessFunctionToolProvider, viewProvider: ViewToolProvider, workflowProvider: WorkflowToolProvider, ) => [ @@ -62,6 +73,8 @@ import { ToolRegistryService } from './services/tool-registry.service'; dashboardProvider, databaseProvider, metadataProvider, + nativeModelProvider, + serverlessFunctionProvider, viewProvider, workflowProvider, ], @@ -70,13 +83,14 @@ import { ToolRegistryService } from './services/tool-registry.service'; DashboardToolProvider, DatabaseToolProvider, MetadataToolProvider, + NativeModelToolProvider, + ServerlessFunctionToolProvider, ViewToolProvider, WorkflowToolProvider, ], }, - ToolProviderService, ToolRegistryService, ], - exports: [ToolProviderService, ToolRegistryService], + exports: [ToolRegistryService], }) export class ToolProviderModule {} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts index 1755a2bb4c..c449989598 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-skill.tool.ts @@ -5,16 +5,14 @@ import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/fla export const LOAD_SKILL_TOOL_NAME = 'load_skill'; export const loadSkillInputSchema = z.object({ - input: z.object({ - skillNames: z - .array(z.string()) - .describe( - 'Names of the skills to load (e.g., ["workflow-building", "data-manipulation"])', - ), - }), + skillNames: z + .array(z.string()) + .describe( + 'Names of the skills to load (e.g., ["workflow-building", "data-manipulation"])', + ), }); -export type LoadSkillInput = z.infer['input']; +export type LoadSkillInput = z.infer; export type LoadSkillResult = { skills: Array<{ @@ -31,10 +29,8 @@ export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({ description: 'Load specialized skills/expertise by name. Returns detailed instructions for workflows, data manipulation, dashboards, metadata, or research.', inputSchema: loadSkillInputSchema, - execute: async (parameters: { - input: LoadSkillInput; - }): Promise => { - const { skillNames } = parameters.input; + execute: async (parameters: LoadSkillInput): Promise => { + const { skillNames } = parameters; const skills = await loadSkills(skillNames); diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-tools.tool.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-tools.tool.ts index 55e0e39542..2b8b41e84e 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-tools.tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tools/load-tools.tool.ts @@ -8,16 +8,14 @@ import { export const LOAD_TOOLS_TOOL_NAME = 'load_tools' as const; export const loadToolsInputSchema = z.object({ - input: z.object({ - toolNames: z - .array(z.string()) - .describe( - 'Array of tool names to load. Use the exact names from the tool catalog.', - ), - }), + toolNames: z + .array(z.string()) + .describe( + 'Array of tool names to load. Use the exact names from the tool catalog.', + ), }); -export type LoadToolsInput = z.infer['input']; +export type LoadToolsInput = z.infer; export type LoadToolsResult = { loaded: string[]; @@ -37,10 +35,8 @@ export const createLoadToolsTool = ( ) => ({ description: `Load tools by name to make them available for use. Call this when you need to use a tool from the catalog that isn't already loaded. You can load multiple tools at once.`, inputSchema: loadToolsInputSchema, - execute: async (parameters: { - input: LoadToolsInput; - }): Promise => { - const { toolNames } = parameters.input; + execute: async (parameters: LoadToolsInput): Promise => { + const { toolNames } = parameters; const loaded: string[] = []; const notFound: string[] = []; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/types/tool-specification.type.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/types/tool-specification.type.ts deleted file mode 100644 index 9ddf48abfa..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/types/tool-specification.type.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type ActorMetadata } from 'twenty-shared/types'; - -import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; - -import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; -import { type ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; -import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; - -export type ToolSpecification = { - workspaceId: string; - categories: ToolCategory[]; - rolePermissionConfig?: RolePermissionConfig; - authContext?: WorkspaceAuthContext; - actorContext?: ActorMetadata; - agent?: FlatAgentWithRoleId | null; - wrapWithErrorContext?: boolean; - // Tools to exclude from the generated toolset (security: prevent recursive code execution) - excludeTools?: ToolType[]; -}; diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts index 253ef0a966..b0b3a75759 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts @@ -14,12 +14,3 @@ export const CodeInterpreterInputZodSchema = z.object({ .optional() .describe('Files to make available in the execution environment'), }); - -export const CodeInterpreterToolParametersZodSchema = z.object({ - loadingMessage: z - .string() - .describe( - "A clear, human-readable status message describing the code being executed. This will be shown to the user while the tool is running, so phrase it as a present-tense status update (e.g., 'Creating a bar chart from sales data'). Explain what analysis or visualization you are performing in natural language.", - ), - input: CodeInterpreterInputZodSchema, -}); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts index 6cb2ebd344..d5f6bb3cdc 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts @@ -24,7 +24,7 @@ import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; -import { CodeInterpreterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema'; +import { CodeInterpreterInputZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema'; import { TWENTY_MCP_HELPER } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const'; import { type CodeInterpreterInput } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type'; import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; @@ -44,7 +44,7 @@ export class CodeInterpreterTool implements Tool { description = 'Execute Python code in a sandboxed environment for data analysis, CSV processing, calculations, and chart generation. Returns stdout, stderr, and generated files. Input files are available at /home/user/{filename}. Save output files (charts, reports) to /home/user/output/ using plt.savefig() for matplotlib charts.'; - inputSchema = CodeInterpreterToolParametersZodSchema; + inputSchema = CodeInterpreterInputZodSchema; constructor( private readonly codeInterpreterService: CodeInterpreterService, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts index 0feb1e97cb..775eab0067 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.schema.ts @@ -14,12 +14,3 @@ export const HttpRequestInputZodSchema = z.object({ .optional() .describe('Request body for POST, PUT, PATCH requests'), }); - -export const HttpToolParametersZodSchema = z.object({ - loadingMessage: z - .string() - .describe( - "A clear, human-readable status message describing the HTTP request being made. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., 'Making a GET request to ...'). Explain what endpoint you are calling and with what parameters in natural language.", - ), - input: HttpRequestInputZodSchema, -}); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts index 2ce665b79f..8f985640e3 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/http-tool/http-tool.ts @@ -4,7 +4,7 @@ import axios, { type AxiosRequestConfig } from 'axios'; import { isDefined } from 'twenty-shared/utils'; import { parseDataFromContentType } from 'twenty-shared/workflow'; -import { HttpToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/http-tool/http-tool.schema'; +import { HttpRequestInputZodSchema } from 'src/engine/core-modules/tool/tools/http-tool/http-tool.schema'; import { type HttpRequestInput } from 'src/engine/core-modules/tool/tools/http-tool/types/http-request-input.type'; import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; @@ -19,7 +19,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent export class HttpTool implements Tool { description = 'Make an HTTP request to any URL with configurable method, headers, and body.'; - inputSchema = HttpToolParametersZodSchema; + inputSchema = HttpRequestInputZodSchema; constructor(private readonly twentyConfigService: TwentyConfigService) {} diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema.ts index a1c8da7324..4d10cda1e2 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema.ts @@ -6,15 +6,6 @@ export const SearchHelpCenterInputZodSchema = z.object({ .describe('The search query to find relevant help articles about Twenty'), }); -export const SearchHelpCenterToolParametersZodSchema = z.object({ - loadingMessage: z - .string() - .describe( - 'A clear, human-readable status message describing the search being performed. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., "Searching help center for..."). Explain what you are searching for in natural language.', - ), - input: SearchHelpCenterInputZodSchema, -}); - export type SearchHelpCenterInput = z.infer< typeof SearchHelpCenterInputZodSchema >; diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.ts index 2dc6e9ed89..8f9ae90e43 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import axios from 'axios'; -import { SearchHelpCenterToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema'; +import { SearchHelpCenterInputZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema'; import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { @@ -15,7 +15,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent export class SearchHelpCenterTool implements Tool { description = 'Search Twenty documentation and help center to find information about features, setup, usage, and troubleshooting.'; - inputSchema = SearchHelpCenterToolParametersZodSchema; + inputSchema = SearchHelpCenterInputZodSchema; constructor(private readonly twentyConfigService: TwentyConfigService) {} diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts index 7f2822af9f..9424fc36f7 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts @@ -1,6 +1,6 @@ import { isValidUuid } from 'twenty-shared/utils'; -import { z } from 'zod'; import { workflowFileSchema } from 'twenty-shared/workflow'; +import { z } from 'zod'; export const SendEmailInputZodSchema = z.object({ email: z.email().describe('The recipient email address'), @@ -19,12 +19,3 @@ export const SendEmailInputZodSchema = z.object({ .optional() .default([]), }); - -export const SendEmailToolParametersZodSchema = z.object({ - loadingMessage: z - .string() - .describe( - "A clear, human-readable status message describing the email being sent. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., 'Sending email to customer about order status'). Explain what email you are sending and to whom in natural language.", - ), - input: SendEmailInputZodSchema, -}); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts index de2214b71a..5d8ab63696 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts @@ -15,7 +15,7 @@ import { SendEmailToolException, SendEmailToolExceptionCode, } from 'src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception'; -import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema'; +import { SendEmailInputZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema'; import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { @@ -36,7 +36,7 @@ export class SendEmailTool implements Tool { description = 'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.'; - inputSchema = SendEmailToolParametersZodSchema; + inputSchema = SendEmailInputZodSchema; constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, diff --git a/packages/twenty-server/src/engine/core-modules/tool/utils/wrap-tool-for-execution.util.ts b/packages/twenty-server/src/engine/core-modules/tool/utils/wrap-tool-for-execution.util.ts new file mode 100644 index 0000000000..710db7b468 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/utils/wrap-tool-for-execution.util.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +const DEFAULT_LOADING_MESSAGE_SCHEMA = z + .string() + .describe( + "A brief status message for the user describing what you're doing (e.g., 'Sending email to customer').", + ); + +// Wraps a flat Zod tool schema with loadingMessage for AI execution +export const wrapSchemaForExecution = ( + schema: z.ZodObject, + customLoadingMessageSchema?: z.ZodString, +): z.ZodObject => { + return z.object({ + loadingMessage: + customLoadingMessageSchema ?? DEFAULT_LOADING_MESSAGE_SCHEMA, + ...schema.shape, + }) as z.ZodObject; +}; + +// For non-Zod schemas (serverless functions with JSON Schema) +export const wrapJsonSchemaForExecution = ( + schema: Record, +): Record => { + const properties = (schema.properties as Record) ?? {}; + const required = (schema.required as string[]) ?? []; + + return { + type: 'object', + properties: { + loadingMessage: { + type: 'string', + description: 'A brief status message for the user.', + }, + ...properties, + }, + required: ['loadingMessage', ...required], + }; +}; + +// Strips loadingMessage from parameters before passing to tool execute +export const stripLoadingMessage = >( + parameters: T, +): Omit => { + const { loadingMessage: _, ...rest } = parameters; + + return rest; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts index 07504e9bc3..f2117c1107 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts @@ -12,9 +12,10 @@ import { type ActorMetadata } from 'twenty-shared/types'; import { type Repository } from 'typeorm'; import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; -import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service'; +import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type'; import { AgentException, @@ -40,7 +41,7 @@ export class AgentAsyncExecutorService { constructor( private readonly aiModelRegistryService: AiModelRegistryService, private readonly agentModelConfigService: AgentModelConfigService, - private readonly toolProvider: ToolProviderService, + private readonly toolRegistry: ToolRegistryService, @InjectRepository(RoleTargetEntity) private readonly roleTargetRepository: Repository, ) {} @@ -119,21 +120,28 @@ export class AgentAsyncExecutorService { // Workflow context: DATABASE_CRUD, ACTION, and NATIVE_MODEL tools only // Workflow tools are excluded to prevent circular dependencies - tools = await this.toolProvider.getTools({ - workspaceId: agent.workspaceId, - categories: [ - ToolCategory.DATABASE_CRUD, - ToolCategory.ACTION, - ToolCategory.NATIVE_MODEL, - ], - rolePermissionConfig: effectiveRoleConfig, - authContext, - actorContext, - agent: agent as unknown as Parameters< - typeof this.toolProvider.getTools - >[0]['agent'], - wrapWithErrorContext: false, - }); + const roleId = this.extractRoleIds(effectiveRoleConfig)[0] ?? ''; + + tools = await this.toolRegistry.getToolsByCategories( + { + workspaceId: agent.workspaceId, + roleId, + rolePermissionConfig: effectiveRoleConfig ?? { unionOf: [] }, + authContext, + actorContext, + agent: agent as unknown as ToolProviderContext['agent'], + userId: authContext?.user?.id, + userWorkspaceId: authContext?.userWorkspaceId, + }, + { + categories: [ + ToolCategory.DATABASE_CRUD, + ToolCategory.ACTION, + ToolCategory.NATIVE_MODEL, + ], + wrapWithErrorContext: false, + }, + ); providerOptions = this.agentModelConfigService.getProviderOptions( registeredModel, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts index 188d4499f5..d297f91081 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts @@ -387,12 +387,13 @@ ${preloadedTools.length > 0 ? preloadedTools.map((t) => `- \`${t}\` ✓`).join(' ### Tool Catalog by Category`); const categoryOrder = [ - 'database', - 'action', - 'workflow', - 'dashboard', - 'metadata', - 'view', + 'DATABASE', + 'ACTION', + 'WORKFLOW', + 'DASHBOARD', + 'METADATA', + 'VIEW', + 'SERVERLESS_FUNCTION', ]; for (const category of categoryOrder) { @@ -426,18 +427,20 @@ ${tools private getCategoryLabel(category: string): string { switch (category) { - case 'database': + case 'DATABASE': return 'Database Tools (CRUD operations)'; - case 'action': + case 'ACTION': return 'Action Tools (HTTP, Email, etc.)'; - case 'workflow': + case 'WORKFLOW': return 'Workflow Tools (create/manage workflows)'; - case 'metadata': + case 'METADATA': return 'Metadata Tools (schema management)'; - case 'view': + case 'VIEW': return 'View Tools (query views)'; - case 'dashboard': + case 'DASHBOARD': return 'Dashboard Tools (create/manage dashboards)'; + case 'SERVERLESS_FUNCTION': + return 'Serverless Functions (custom tools)'; default: return category; } 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 5e7091dc1b..ddac652f91 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 @@ -10,134 +10,98 @@ import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-mod import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; const GetFieldMetadataInputSchema = z.object({ - loadingMessage: z + id: z .string() + .uuid() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z - .string() - .uuid() - .optional() - .describe( - 'Unique identifier for the field metadata. If provided, returns a single field.', - ), - objectMetadataId: z - .string() - .uuid() - .optional() - .describe('Filter fields by object metadata ID.'), - limit: z - .number() - .int() - .min(1) - .max(100) - .default(100) - .describe('Maximum number of fields to return.'), - }), + .describe( + 'Unique identifier for the field metadata. If provided, returns a single field.', + ), + objectMetadataId: z + .string() + .uuid() + .optional() + .describe('Filter fields by object metadata ID.'), + limit: z + .number() + .int() + .min(1) + .max(100) + .default(100) + .describe('Maximum number of fields to return.'), }); const CreateFieldMetadataInputSchema = z.object({ - loadingMessage: z + objectMetadataId: z .string() + .uuid() + .describe('ID of the object to add the field to'), + type: z + .nativeEnum(FieldMetadataType) + .describe( + 'Field type (e.g., TEXT, NUMBER, BOOLEAN, DATE_TIME, RELATION, etc.)', + ), + name: z.string().describe('Internal name of the field (camelCase)'), + label: z.string().describe('Display label of the field'), + description: z.string().optional().describe('Description of the field'), + icon: z.string().optional().describe('Icon identifier for the field'), + isNullable: z.boolean().optional().describe('Whether the field can be null'), + isUnique: z + .boolean() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - objectMetadataId: z - .string() - .uuid() - .describe('ID of the object to add the field to'), - type: z - .nativeEnum(FieldMetadataType) - .describe( - 'Field type (e.g., TEXT, NUMBER, BOOLEAN, DATE_TIME, RELATION, etc.)', - ), - name: z.string().describe('Internal name of the field (camelCase)'), - label: z.string().describe('Display label of the field'), - description: z.string().optional().describe('Description of the field'), - icon: z.string().optional().describe('Icon identifier for the field'), - isNullable: z - .boolean() - .optional() - .describe('Whether the field can be null'), - isUnique: z - .boolean() - .optional() - .describe('Whether the field value must be unique'), - defaultValue: z - .unknown() - .optional() - .describe('Default value for the field'), - options: z - .unknown() - .optional() - .describe('Options for SELECT/MULTI_SELECT fields'), - settings: z - .unknown() - .optional() - .describe('Additional settings for the field'), - isLabelSyncedWithName: z - .boolean() - .optional() - .describe('Whether label should sync with name changes'), - isRemoteCreation: z - .boolean() - .optional() - .describe('Whether this is a remote field creation'), - relationCreationPayload: z - .unknown() - .optional() - .describe('Payload for creating relation fields'), - }), + .describe('Whether the field value must be unique'), + defaultValue: z.unknown().optional().describe('Default value for the field'), + options: z + .unknown() + .optional() + .describe('Options for SELECT/MULTI_SELECT fields'), + settings: z + .unknown() + .optional() + .describe('Additional settings for the field'), + isLabelSyncedWithName: z + .boolean() + .optional() + .describe('Whether label should sync with name changes'), + isRemoteCreation: z + .boolean() + .optional() + .describe('Whether this is a remote field creation'), + relationCreationPayload: z + .unknown() + .optional() + .describe('Payload for creating relation fields'), }); const UpdateFieldMetadataInputSchema = z.object({ - loadingMessage: z - .string() + id: z.string().uuid().describe('ID of the field to update'), + name: z.string().optional().describe('Internal name of the field'), + label: z.string().optional().describe('Display label of the field'), + description: z.string().optional().describe('Description of the field'), + icon: z.string().optional().describe('Icon identifier for the field'), + isActive: z.boolean().optional().describe('Whether the field is active'), + isNullable: z.boolean().optional().describe('Whether the field can be null'), + isUnique: z + .boolean() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z.string().uuid().describe('ID of the field to update'), - name: z.string().optional().describe('Internal name of the field'), - label: z.string().optional().describe('Display label of the field'), - description: z.string().optional().describe('Description of the field'), - icon: z.string().optional().describe('Icon identifier for the field'), - isActive: z.boolean().optional().describe('Whether the field is active'), - isNullable: z - .boolean() - .optional() - .describe('Whether the field can be null'), - isUnique: z - .boolean() - .optional() - .describe('Whether the field value must be unique'), - defaultValue: z - .unknown() - .optional() - .describe('Default value for the field'), - options: z - .unknown() - .optional() - .describe('Options for SELECT/MULTI_SELECT fields'), - settings: z - .unknown() - .optional() - .describe('Additional settings for the field'), - isLabelSyncedWithName: z - .boolean() - .optional() - .describe('Whether label should sync with name changes'), - }), + .describe('Whether the field value must be unique'), + defaultValue: z.unknown().optional().describe('Default value for the field'), + options: z + .unknown() + .optional() + .describe('Options for SELECT/MULTI_SELECT fields'), + settings: z + .unknown() + .optional() + .describe('Additional settings for the field'), + isLabelSyncedWithName: z + .boolean() + .optional() + .describe('Whether label should sync with name changes'), }); const DeleteFieldMetadataInputSchema = z.object({ - loadingMessage: z - .string() - .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z.string().uuid().describe('ID of the field to delete'), - }), + id: z.string().uuid().describe('ID of the field to delete'), }); @Injectable() @@ -151,21 +115,21 @@ export class FieldMetadataToolsFactory { 'Find fields metadata. Retrieve information about the fields of objects in the workspace data model.', inputSchema: GetFieldMetadataInputSchema, execute: async (parameters: { - input: { id?: string; objectMetadataId?: string; limit?: number }; + id?: string; + objectMetadataId?: string; + limit?: number; }) => { return this.fieldMetadataService.query({ filter: { workspaceId: { eq: workspaceId }, - ...(parameters.input.id - ? { id: { eq: parameters.input.id } } - : {}), - ...(parameters.input.objectMetadataId + ...(parameters.id ? { id: { eq: parameters.id } } : {}), + ...(parameters.objectMetadataId ? { - objectMetadataId: { eq: parameters.input.objectMetadataId }, + objectMetadataId: { eq: parameters.objectMetadataId }, } : {}), }, - paging: { limit: parameters.input.limit ?? 100 }, + paging: { limit: parameters.limit ?? 100 }, }); }, }, @@ -174,27 +138,25 @@ export class FieldMetadataToolsFactory { 'Create a new field metadata on an object. Specify the objectMetadataId and field properties.', inputSchema: CreateFieldMetadataInputSchema, execute: async (parameters: { - input: { - objectMetadataId: string; - type: FieldMetadataType; - name: string; - label: string; - description?: string; - icon?: string; - isNullable?: boolean; - isUnique?: boolean; - defaultValue?: unknown; - options?: unknown; - settings?: unknown; - isLabelSyncedWithName?: boolean; - isRemoteCreation?: boolean; - relationCreationPayload?: unknown; - }; + objectMetadataId: string; + type: FieldMetadataType; + name: string; + label: string; + description?: string; + icon?: string; + isNullable?: boolean; + isUnique?: boolean; + defaultValue?: unknown; + options?: unknown; + settings?: unknown; + isLabelSyncedWithName?: boolean; + isRemoteCreation?: boolean; + relationCreationPayload?: unknown; }) => { try { const flatFieldMetadata = await this.fieldMetadataService.createOneField({ - createFieldInput: parameters.input as Parameters< + createFieldInput: parameters as Parameters< typeof this.fieldMetadataService.createOneField >[0]['createFieldInput'], workspaceId, @@ -214,23 +176,21 @@ export class FieldMetadataToolsFactory { 'Update an existing field metadata. Provide the field ID and the properties to update.', inputSchema: UpdateFieldMetadataInputSchema, execute: async (parameters: { - input: { - id: string; - name?: string; - label?: string; - description?: string; - icon?: string; - isActive?: boolean; - isNullable?: boolean; - isUnique?: boolean; - defaultValue?: unknown; - options?: unknown; - settings?: unknown; - isLabelSyncedWithName?: boolean; - }; + id: string; + name?: string; + label?: string; + description?: string; + icon?: string; + isActive?: boolean; + isNullable?: boolean; + isUnique?: boolean; + defaultValue?: unknown; + options?: unknown; + settings?: unknown; + isLabelSyncedWithName?: boolean; }) => { try { - const { id, ...update } = parameters.input; + const { id, ...update } = parameters; const flatFieldMetadata = await this.fieldMetadataService.updateOneField({ @@ -252,11 +212,11 @@ export class FieldMetadataToolsFactory { delete_field_metadata: { description: 'Delete a field metadata by its ID.', inputSchema: DeleteFieldMetadataInputSchema, - execute: async (parameters: { input: { id: string } }) => { + execute: async (parameters: { id: string }) => { try { const flatFieldMetadata = await this.fieldMetadataService.deleteOneField({ - deleteOneFieldInput: { id: parameters.input.id }, + deleteOneFieldInput: { id: parameters.id }, workspaceId, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index b12806ba06..8259506630 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -80,7 +80,7 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { ), 'deletedAt', ], - propertiesToStringify: [], + propertiesToStringify: ['toolInputSchema'], }, cronTrigger: { propertiesToCompare: [...FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES], 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 3843be2a3b..6229c9a77a 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 @@ -9,112 +9,76 @@ import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metada import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; const GetObjectMetadataInputSchema = z.object({ - loadingMessage: z + id: z .string() + .uuid() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z - .string() - .uuid() - .optional() - .describe( - 'Unique identifier for the object metadata. If provided, returns a single object.', - ), - limit: z - .number() - .int() - .min(1) - .max(100) - .default(100) - .describe('Maximum number of objects to return.'), - }), + .describe( + 'Unique identifier for the object metadata. If provided, returns a single object.', + ), + limit: z + .number() + .int() + .min(1) + .max(100) + .default(100) + .describe('Maximum number of objects to return.'), }); const CreateObjectMetadataInputSchema = z.object({ - loadingMessage: z + nameSingular: z .string() + .describe('Singular name for the object (e.g., "company")'), + namePlural: z + .string() + .describe('Plural name for the object (e.g., "companies")'), + labelSingular: z + .string() + .describe('Display label in singular form (e.g., "Company")'), + labelPlural: z + .string() + .describe('Display label in plural form (e.g., "Companies")'), + description: z.string().optional().describe('Description of the object'), + icon: z.string().optional().describe('Icon identifier for the object'), + shortcut: z.string().optional().describe('Keyboard shortcut for the object'), + isRemote: z.boolean().optional().describe('Whether this is a remote object'), + isLabelSyncedWithName: z + .boolean() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - nameSingular: z - .string() - .describe('Singular name for the object (e.g., "company")'), - namePlural: z - .string() - .describe('Plural name for the object (e.g., "companies")'), - labelSingular: z - .string() - .describe('Display label in singular form (e.g., "Company")'), - labelPlural: z - .string() - .describe('Display label in plural form (e.g., "Companies")'), - description: z.string().optional().describe('Description of the object'), - icon: z.string().optional().describe('Icon identifier for the object'), - shortcut: z - .string() - .optional() - .describe('Keyboard shortcut for the object'), - isRemote: z - .boolean() - .optional() - .describe('Whether this is a remote object'), - isLabelSyncedWithName: z - .boolean() - .optional() - .describe('Whether label should sync with name changes'), - }), + .describe('Whether label should sync with name changes'), }); const UpdateObjectMetadataInputSchema = z.object({ - loadingMessage: z + id: z.string().uuid().describe('ID of the object to update'), + labelSingular: z .string() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z.string().uuid().describe('ID of the object to update'), - labelSingular: z - .string() - .optional() - .describe('Display label in singular form'), - labelPlural: z.string().optional().describe('Display label in plural form'), - nameSingular: z - .string() - .optional() - .describe('Singular name for the object'), - namePlural: z.string().optional().describe('Plural name for the object'), - description: z.string().optional().describe('Description of the object'), - icon: z.string().optional().describe('Icon identifier for the object'), - shortcut: z - .string() - .optional() - .describe('Keyboard shortcut for the object'), - isActive: z.boolean().optional().describe('Whether the object is active'), - labelIdentifierFieldMetadataId: z - .string() - .uuid() - .optional() - .describe('ID of the field used as label identifier'), - imageIdentifierFieldMetadataId: z - .string() - .uuid() - .optional() - .describe('ID of the field used as image identifier'), - isLabelSyncedWithName: z - .boolean() - .optional() - .describe('Whether label should sync with name changes'), - }), + .describe('Display label in singular form'), + labelPlural: z.string().optional().describe('Display label in plural form'), + nameSingular: z.string().optional().describe('Singular name for the object'), + namePlural: z.string().optional().describe('Plural name for the object'), + description: z.string().optional().describe('Description of the object'), + icon: z.string().optional().describe('Icon identifier for the object'), + shortcut: z.string().optional().describe('Keyboard shortcut for the object'), + isActive: z.boolean().optional().describe('Whether the object is active'), + labelIdentifierFieldMetadataId: z + .string() + .uuid() + .optional() + .describe('ID of the field used as label identifier'), + imageIdentifierFieldMetadataId: z + .string() + .uuid() + .optional() + .describe('ID of the field used as image identifier'), + isLabelSyncedWithName: z + .boolean() + .optional() + .describe('Whether label should sync with name changes'), }); const DeleteObjectMetadataInputSchema = z.object({ - loadingMessage: z - .string() - .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z.string().uuid().describe('ID of the object to delete'), - }), + id: z.string().uuid().describe('ID of the object to delete'), }); @Injectable() @@ -127,17 +91,13 @@ export class ObjectMetadataToolsFactory { description: 'Find objects metadata. Retrieve information about the data model objects in the workspace.', inputSchema: GetObjectMetadataInputSchema, - execute: async (parameters: { - input: { id?: string; limit?: number }; - }) => { + execute: async (parameters: { id?: string; limit?: number }) => { const flatObjectMetadatas = await this.objectMetadataService.findManyWithinWorkspace( workspaceId, { - ...(parameters.input.id - ? { where: { id: parameters.input.id } } - : {}), - take: parameters.input.limit ?? 100, + ...(parameters.id ? { where: { id: parameters.id } } : {}), + take: parameters.limit ?? 100, }, ); @@ -151,22 +111,20 @@ export class ObjectMetadataToolsFactory { 'Create a new object metadata in the workspace data model.', inputSchema: CreateObjectMetadataInputSchema, execute: async (parameters: { - input: { - nameSingular: string; - namePlural: string; - labelSingular: string; - labelPlural: string; - description?: string; - icon?: string; - shortcut?: string; - isRemote?: boolean; - isLabelSyncedWithName?: boolean; - }; + nameSingular: string; + namePlural: string; + labelSingular: string; + labelPlural: string; + description?: string; + icon?: string; + shortcut?: string; + isRemote?: boolean; + isLabelSyncedWithName?: boolean; }) => { try { const flatObjectMetadata = await this.objectMetadataService.createOneObject({ - createObjectInput: parameters.input as Parameters< + createObjectInput: parameters as Parameters< typeof this.objectMetadataService.createOneObject >[0]['createObjectInput'], workspaceId, @@ -188,23 +146,21 @@ export class ObjectMetadataToolsFactory { 'Update an existing object metadata. Provide the object ID and the fields to update.', inputSchema: UpdateObjectMetadataInputSchema, execute: async (parameters: { - input: { - id: string; - labelSingular?: string; - labelPlural?: string; - nameSingular?: string; - namePlural?: string; - description?: string; - icon?: string; - shortcut?: string; - isActive?: boolean; - labelIdentifierFieldMetadataId?: string; - imageIdentifierFieldMetadataId?: string; - isLabelSyncedWithName?: boolean; - }; + id: string; + labelSingular?: string; + labelPlural?: string; + nameSingular?: string; + namePlural?: string; + description?: string; + icon?: string; + shortcut?: string; + isActive?: boolean; + labelIdentifierFieldMetadataId?: string; + imageIdentifierFieldMetadataId?: string; + isLabelSyncedWithName?: boolean; }) => { try { - const { id, ...update } = parameters.input; + const { id, ...update } = parameters; const flatObjectMetadata = await this.objectMetadataService.updateOneObject({ @@ -227,11 +183,11 @@ export class ObjectMetadataToolsFactory { description: 'Delete an object metadata by its ID. This will also delete all associated fields.', inputSchema: DeleteObjectMetadataInputSchema, - execute: async (parameters: { input: { id: string } }) => { + execute: async (parameters: { id: string }) => { try { const flatObjectMetadata = await this.objectMetadataService.deleteOneObject({ - deleteObjectInput: { id: parameters.input.id }, + deleteObjectInput: { id: parameters.id }, workspaceId, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant.ts new file mode 100644 index 0000000000..f0824a6572 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant.ts @@ -0,0 +1,9 @@ +// Default tool input schema matching the base-typescript-project template +// Template params: { a: string; b: number; } +export const DEFAULT_TOOL_INPUT_SCHEMA = { + type: 'object', + properties: { + a: { type: 'string' }, + b: { type: 'number' }, + }, +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts index 78dcaf5948..8b2a95584e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts @@ -8,4 +8,6 @@ export const FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES = [ 'code', 'handlerPath', 'handlerName', + 'toolInputSchema', + 'isTool', ] as const satisfies (keyof FlatServerlessFunction)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts index f6b4795e96..8369dbfda8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts @@ -1,6 +1,7 @@ import { Field, HideField, InputType } from '@nestjs/graphql'; import { + IsBoolean, IsNotEmpty, IsNumber, IsObject, @@ -54,4 +55,14 @@ export class CreateServerlessFunctionInput { @Field({ nullable: true }) @IsOptional() handlerPath?: string; + + @Field(() => graphqlTypeJson, { nullable: true }) + @IsObject() + @IsOptional() + toolInputSchema?: object; + + @IsBoolean() + @Field({ nullable: true }) + @IsOptional() + isTool?: boolean; } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts index a10a5f05af..8a36af98cf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts @@ -7,12 +7,16 @@ import { } from '@ptc-org/nestjs-query-graphql'; import { IsArray, + IsBoolean, IsDateString, IsNotEmpty, IsNumber, + IsObject, + IsOptional, IsString, IsUUID, } from 'class-validator'; +import graphqlTypeJson from 'graphql-type-json'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto'; @@ -69,6 +73,15 @@ export class ServerlessFunctionDTO { @Field(() => [String], { nullable: false }) publishedVersions: string[]; + @IsObject() + @IsOptional() + @Field(() => graphqlTypeJson, { nullable: true }) + toolInputSchema?: object; + + @IsBoolean() + @Field() + isTool: boolean; + @Field(() => [CronTriggerDTO], { nullable: true }) cronTriggers?: CronTriggerDTO[]; @@ -78,6 +91,11 @@ export class ServerlessFunctionDTO { @Field(() => [RouteTriggerDTO], { nullable: true }) routeTriggers?: RouteTriggerDTO[]; + @IsUUID() + @IsOptional() + @Field(() => UUIDScalarType, { nullable: true }) + applicationId?: string; + @HideField() workspaceId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts index 70ff36a284..aabb6c6130 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts @@ -2,6 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { Type } from 'class-transformer'; import { + IsBoolean, IsNotEmpty, IsNumber, IsObject, @@ -50,6 +51,16 @@ class UpdateServerlessFunctionInputUpdates { @Field({ nullable: true }) @IsOptional() handlerPath?: string; + + @Field(() => graphqlTypeJson, { nullable: true }) + @IsObject() + @IsOptional() + toolInputSchema?: object; + + @IsBoolean() + @Field({ nullable: true }) + @IsOptional() + isTool?: boolean; } @InputType() diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts index b818a61e7f..617df12b6c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -66,6 +66,12 @@ export class ServerlessFunctionEntity @Column({ nullable: true, type: 'text' }) checksum: string | null; + @Column({ nullable: true, type: 'jsonb' }) + toolInputSchema: object | null; + + @Column({ nullable: false, default: false }) + isTool: boolean; + @Column({ nullable: false, type: 'uuid' }) serverlessFunctionLayerId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index b90050e36d..ac35213d6d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -4,26 +4,29 @@ import { InjectRepository } from '@nestjs/typeorm'; import { join } from 'path'; import deepEqual from 'deep-equal'; -import { isDefined } from 'twenty-shared/utils'; -import { IsNull, Not, Repository } from 'typeorm'; -import { Sources } from 'twenty-shared/types'; import { DEFAULT_API_KEY_NAME, DEFAULT_API_URL_NAME, } from 'twenty-shared/application'; +import { Sources } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { IsNull, Not, Repository } from 'typeorm'; import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed'; +import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var'; import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; +import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; @@ -31,15 +34,13 @@ import { ServerlessFunctionException, ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; +import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; +import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, } from 'src/modules/workflow/common/exceptions/workflow-version-step.exception'; -import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; -import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var'; import { cleanServerUrl } from 'src/utils/clean-server-url'; -import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; -import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5; @@ -334,6 +335,8 @@ export class ServerlessFunctionService { name: serverlessFunctionInput.update.name, description: serverlessFunctionInput.update.description, timeoutSeconds: serverlessFunctionInput.update.timeoutSeconds, + toolInputSchema: serverlessFunctionInput.update.toolInputSchema, + isTool: serverlessFunctionInput.update.isTool, }, ); @@ -405,8 +408,14 @@ export class ServerlessFunctionService { serverlessFunctionLayerId: serverlessFunctionToCreateLayerId, }; + // If no toolInputSchema is provided, use the default schema + // (because the default template will be used for the code) + const toolInputSchema = isDefined(serverlessFunctionInput.toolInputSchema) + ? serverlessFunctionInput.toolInputSchema + : DEFAULT_TOOL_INPUT_SCHEMA; + const serverlessFunctionToCreate = this.serverlessFunctionRepository.create( - { ...createServerlessFunctionInput, workspaceId }, + { ...createServerlessFunctionInput, workspaceId, toolInputSchema }, ); const createdServerlessFunction = diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts index 1ce791d3cd..60414cb32e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts @@ -1,5 +1,7 @@ import { v4 } from 'uuid'; +import { isDefined } from 'twenty-shared/utils'; +import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant'; import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { DEFAULT_HANDLER_NAME, @@ -55,5 +57,15 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({ JSON.stringify(rawCreateServerlessFunctionInput.code), ) : null, + // If no schema provided and no code provided, use default schema + // (because the default template will be used) + toolInputSchema: isDefined( + rawCreateServerlessFunctionInput?.toolInputSchema, + ) + ? rawCreateServerlessFunctionInput.toolInputSchema + : !isDefined(rawCreateServerlessFunctionInput?.code) + ? DEFAULT_TOOL_INPUT_SCHEMA + : null, + isTool: rawCreateServerlessFunctionInput?.isTool ?? false, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts b/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts index 12b338ca7b..f0ae0444db 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts @@ -120,7 +120,7 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['get_views'], { - input: { limit: 50 }, + limit: 50, }); expect(viewService.findByWorkspaceId).toHaveBeenCalledWith( @@ -150,7 +150,8 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['get_views'], { - input: { objectNameSingular: mockObjectNameSingular, limit: 50 }, + objectNameSingular: mockObjectNameSingular, + limit: 50, }); expect(viewService.findByObjectMetadataId).toHaveBeenCalledWith( @@ -173,7 +174,7 @@ describe('ViewToolsFactory', () => { const tools = viewToolsFactory.generateReadTools(mockWorkspaceId); const result = await callExecute(tools['get_views'], { - input: { limit: 2 }, + limit: 2, }); expect(result).toHaveLength(2); @@ -201,7 +202,7 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['get_view_query_parameters'], { - input: { viewId: mockViewId }, + viewId: mockViewId, }); expect( @@ -244,11 +245,9 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['create_view'], { - input: { - name: 'New View', - objectNameSingular: mockObjectNameSingular, - icon: 'IconTable', - }, + name: 'New View', + objectNameSingular: mockObjectNameSingular, + icon: 'IconTable', }); expect(viewService.createOne).toHaveBeenCalledWith({ @@ -293,10 +292,8 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['update_view'], { - input: { - id: mockViewId, - name: 'Updated Name', - }, + id: mockViewId, + name: 'Updated Name', }); expect(viewService.updateOne).toHaveBeenCalled(); @@ -323,10 +320,8 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['update_view'], { - input: { - id: mockViewId, - name: 'Updated Name', - }, + id: mockViewId, + name: 'Updated Name', }); expect(result.name).toBe('Updated Name'); @@ -348,10 +343,8 @@ describe('ViewToolsFactory', () => { await expect( callExecute(tools['update_view'], { - input: { - id: mockViewId, - name: 'Updated Name', - }, + id: mockViewId, + name: 'Updated Name', }), ).rejects.toThrow('You can only update your own unlisted views'); }); @@ -363,10 +356,8 @@ describe('ViewToolsFactory', () => { await expect( callExecute(tools['update_view'], { - input: { - id: 'non-existent-id', - name: 'Updated Name', - }, + id: 'non-existent-id', + name: 'Updated Name', }), ).rejects.toThrow('View with id non-existent-id not found'); }); @@ -392,7 +383,7 @@ describe('ViewToolsFactory', () => { ); const result = await callExecute(tools['delete_view'], { - input: { id: mockViewId }, + id: mockViewId, }); expect(viewService.deleteOne).toHaveBeenCalledWith({ @@ -422,7 +413,7 @@ describe('ViewToolsFactory', () => { await expect( callExecute(tools['delete_view'], { - input: { id: mockViewId }, + id: mockViewId, }), ).rejects.toThrow('You can only delete your own unlisted views'); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts index 54e9d9eee1..f1590ee09e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts @@ -12,90 +12,60 @@ import { ViewService } from 'src/engine/metadata-modules/view/services/view.serv import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; const GetViewsInputSchema = z.object({ - loadingMessage: z + objectNameSingular: z .string() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - objectNameSingular: z - .string() - .optional() - .describe( - 'Filter views by object name (e.g., "task", "person", "company"). If omitted, returns all views.', - ), - limit: z - .number() - .int() - .min(1) - .max(100) - .default(50) - .describe('Maximum views to return.'), - }), + .describe( + 'Filter views by object name (e.g., "task", "person", "company"). If omitted, returns all views.', + ), + limit: z + .number() + .int() + .min(1) + .max(100) + .default(50) + .describe('Maximum views to return.'), }); const GetViewQueryParamsInputSchema = z.object({ - loadingMessage: z + viewId: z .string() - .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - viewId: z - .string() - .uuid() - .describe('ID of the view to get query parameters for.'), - }), + .uuid() + .describe('ID of the view to get query parameters for.'), }); const CreateViewInputSchema = z.object({ - loadingMessage: z + name: z.string().describe('View name'), + objectNameSingular: z + .string() + .describe( + 'Object name this view is for (e.g., "task", "person", "company")', + ), + icon: z .string() .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - name: z.string().describe('View name'), - objectNameSingular: z - .string() - .describe( - 'Object name this view is for (e.g., "task", "person", "company")', - ), - icon: z - .string() - .optional() - .default('IconList') - .describe('Icon identifier (e.g., "IconList", "IconCheckbox")'), - type: z - .enum([ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR]) - .optional() - .default(ViewType.TABLE) - .describe('View type'), - visibility: z - .enum([ViewVisibility.WORKSPACE, ViewVisibility.UNLISTED]) - .optional() - .default(ViewVisibility.WORKSPACE) - .describe('View visibility'), - }), + .default('IconList') + .describe('Icon identifier (e.g., "IconList", "IconCheckbox")'), + type: z + .enum([ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR]) + .optional() + .default(ViewType.TABLE) + .describe('View type'), + visibility: z + .enum([ViewVisibility.WORKSPACE, ViewVisibility.UNLISTED]) + .optional() + .default(ViewVisibility.WORKSPACE) + .describe('View visibility'), }); const UpdateViewInputSchema = z.object({ - loadingMessage: z - .string() - .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z.string().uuid().describe('View ID to update'), - name: z.string().optional().describe('New view name'), - icon: z.string().optional().describe('New icon identifier'), - }), + id: z.string().uuid().describe('View ID to update'), + name: z.string().optional().describe('New view name'), + icon: z.string().optional().describe('New icon identifier'), }); const DeleteViewInputSchema = z.object({ - loadingMessage: z - .string() - .optional() - .describe('A clear description of the action being performed.'), - input: z.object({ - id: z.string().uuid().describe('View ID to delete'), - }), + id: z.string().uuid().describe('View ID to delete'), }); @Injectable() @@ -142,14 +112,15 @@ export class ViewToolsFactory { 'List views in the workspace. Views define how records are displayed, filtered, and sorted.', inputSchema: GetViewsInputSchema, execute: async (parameters: { - input: { objectNameSingular?: string; limit?: number }; + objectNameSingular?: string; + limit?: number; }) => { let views; - if (parameters.input.objectNameSingular) { + if (parameters.objectNameSingular) { const objectMetadataId = await this.resolveObjectMetadataId( workspaceId, - parameters.input.objectNameSingular, + parameters.objectNameSingular, ); views = await this.viewService.findByObjectMetadataId( @@ -164,7 +135,7 @@ export class ViewToolsFactory { ); } - const limitedViews = views.slice(0, parameters.input.limit ?? 50); + const limitedViews = views.slice(0, parameters.limit ?? 50); return limitedViews.map((view) => ({ id: view.id, @@ -181,9 +152,9 @@ export class ViewToolsFactory { description: 'Get filter and sort parameters from a view. Use these parameters with find_* tools to query records matching the view.', inputSchema: GetViewQueryParamsInputSchema, - execute: async (parameters: { input: { viewId: string } }) => { + execute: async (parameters: { viewId: string }) => { return this.viewQueryParamsService.resolveViewToQueryParams( - parameters.input.viewId, + parameters.viewId, workspaceId, currentWorkspaceMemberId, ); @@ -199,28 +170,25 @@ export class ViewToolsFactory { 'Create a new view for an object. Views define how records are displayed.', inputSchema: CreateViewInputSchema, execute: async (parameters: { - input: { - name: string; - objectNameSingular: string; - icon?: string; - type?: ViewType; - visibility?: ViewVisibility; - }; + name: string; + objectNameSingular: string; + icon?: string; + type?: ViewType; + visibility?: ViewVisibility; }) => { try { const objectMetadataId = await this.resolveObjectMetadataId( workspaceId, - parameters.input.objectNameSingular, + parameters.objectNameSingular, ); const view = await this.viewService.createOne({ createViewInput: { - name: parameters.input.name, + name: parameters.name, objectMetadataId, - icon: parameters.input.icon ?? 'IconList', - type: parameters.input.type ?? ViewType.TABLE, - visibility: - parameters.input.visibility ?? ViewVisibility.WORKSPACE, + icon: parameters.icon ?? 'IconList', + type: parameters.type ?? ViewType.TABLE, + visibility: parameters.visibility ?? ViewVisibility.WORKSPACE, }, workspaceId, createdByUserWorkspaceId: userWorkspaceId, @@ -229,7 +197,7 @@ export class ViewToolsFactory { return { id: view.id, name: view.name, - objectNameSingular: parameters.input.objectNameSingular, + objectNameSingular: parameters.objectNameSingular, type: view.type, icon: view.icon, visibility: view.visibility, @@ -247,20 +215,18 @@ export class ViewToolsFactory { 'Update an existing view. You can change the name and icon.', inputSchema: UpdateViewInputSchema, execute: async (parameters: { - input: { - id: string; - name?: string; - icon?: string; - }; + id: string; + name?: string; + icon?: string; }) => { try { const existingView = await this.viewService.findById( - parameters.input.id, + parameters.id, workspaceId, ); if (!existingView) { - throw new Error(`View with id ${parameters.input.id} not found`); + throw new Error(`View with id ${parameters.id} not found`); } if ( @@ -272,9 +238,9 @@ export class ViewToolsFactory { const view = await this.viewService.updateOne({ updateViewInput: { - id: parameters.input.id, - name: parameters.input.name, - icon: parameters.input.icon, + id: parameters.id, + name: parameters.name, + icon: parameters.icon, }, workspaceId, userWorkspaceId, @@ -299,15 +265,15 @@ export class ViewToolsFactory { delete_view: { description: 'Delete a view by its ID.', inputSchema: DeleteViewInputSchema, - execute: async (parameters: { input: { id: string } }) => { + execute: async (parameters: { id: string }) => { try { const existingView = await this.viewService.findById( - parameters.input.id, + parameters.id, workspaceId, ); if (!existingView) { - throw new Error(`View with id ${parameters.input.id} not found`); + throw new Error(`View with id ${parameters.id} not found`); } if ( @@ -318,7 +284,7 @@ export class ViewToolsFactory { } const view = await this.viewService.deleteOne({ - deleteViewInput: { id: parameters.input.id }, + deleteViewInput: { id: parameters.id }, workspaceId, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts index 9ac3efbda7..5a71a30e57 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts @@ -3,7 +3,6 @@ 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 { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-skill-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'; @@ -18,6 +17,7 @@ import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/w import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-row-level-permission-predicate-group-validator.service'; import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-row-level-permission-predicate-validator.service'; import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-serverless-function-validator.service'; +import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-skill-validator.service'; import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service'; import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-group-validator.service'; import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service'; diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index 12d30361ed..4f0cfcb68e 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -17,6 +17,7 @@ export type { ObjectManifest } from './objectManifestType'; export type { PackageJson } from './packageJsonType'; export type { RoleManifest } from './roleManifestType'; export type { + InputJsonSchema, ServerlessFunctionManifest, DatabaseEventTrigger, CronTrigger, diff --git a/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts b/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts index 9bf91a006d..80d8f5b2c6 100644 --- a/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts +++ b/packages/twenty-shared/src/application/serverlessFunctionManifestType.ts @@ -1,5 +1,23 @@ -import { type HTTPMethod } from '@/types'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; +import { type HTTPMethod } from '@/types'; + +// Standard JSON Schema type for tool input/output definitions +export type InputJsonSchema = { + type?: + | 'string' + | 'number' + | 'boolean' + | 'object' + | 'array' + | 'integer' + | 'null'; + description?: string; + enum?: unknown[]; + items?: InputJsonSchema; + properties?: Record; + required?: string[]; + additionalProperties?: boolean | InputJsonSchema; +}; export type ServerlessFunctionManifest = SyncableEntityOptions & { name?: string; @@ -8,6 +26,8 @@ export type ServerlessFunctionManifest = SyncableEntityOptions & { triggers: ServerlessFunctionTriggerManifest[]; handlerPath: string; handlerName: string; + toolInputSchema?: InputJsonSchema; + isTool?: boolean; }; export type DatabaseEventTrigger = { diff --git a/packages/twenty-shared/src/index.ts b/packages/twenty-shared/src/index.ts index 5d375033ad..4b2fef6e10 100644 --- a/packages/twenty-shared/src/index.ts +++ b/packages/twenty-shared/src/index.ts @@ -7,5 +7,4 @@ * |___/ */ - export default {};