feat: Serverless Functions as AI Tools (#16919)
## Summary This PR enables serverless functions to be exposed as AI tools, allowing them to be used by AI agents. ### Changes - Added new `SERVERLESS_FUNCTION` tool category - Added `toolDescription`, `toolInputSchema`, and `toolOutputSchema` fields to serverless functions - Created database migration for the new schema columns - Added tool index query and resolver for fetching available tools - Added Settings AI page tabs (Skills, Tools, Settings) with new tools table - Added utility to convert tool schema to JSON schema format - Updated frontend to display tools in the settings page ### Implementation Details - Serverless functions can now define tool metadata (description, input/output schemas) - These functions are automatically registered in the tool registry - The tool index endpoint allows querying available tools with their schemas - Settings page now has a dedicated Tools tab showing all available tools
This commit is contained in:
@@ -968,8 +968,10 @@ export type CreateServerlessFunctionInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
handlerName?: InputMaybe<Scalars['String']>;
|
||||
handlerPath?: InputMaybe<Scalars['String']>;
|
||||
isTool?: InputMaybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
timeoutSeconds?: InputMaybe<Scalars['Float']>;
|
||||
toolInputSchema?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type CreateSkillInput = {
|
||||
@@ -3422,6 +3424,7 @@ export type Query = {
|
||||
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
getToolIndex: Array<ToolIndexEntry>;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
listPlans: Array<BillingPlanOutput>;
|
||||
@@ -4103,6 +4106,7 @@ export type Sentry = {
|
||||
|
||||
export type ServerlessFunction = {
|
||||
__typename?: 'ServerlessFunction';
|
||||
applicationId?: Maybe<Scalars['UUID']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
cronTriggers?: Maybe<Array<CronTrigger>>;
|
||||
databaseEventTriggers?: Maybe<Array<DatabaseEventTrigger>>;
|
||||
@@ -4110,12 +4114,14 @@ export type ServerlessFunction = {
|
||||
handlerName: Scalars['String'];
|
||||
handlerPath: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
isTool: Scalars['Boolean'];
|
||||
latestVersion?: Maybe<Scalars['String']>;
|
||||
name: Scalars['String'];
|
||||
publishedVersions: Array<Scalars['String']>;
|
||||
routeTriggers?: Maybe<Array<RouteTrigger>>;
|
||||
runtime: Scalars['String'];
|
||||
timeoutSeconds: Scalars['Float'];
|
||||
toolInputSchema?: Maybe<Scalars['JSON']>;
|
||||
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<Scalars['JSON']>;
|
||||
name: Scalars['String'];
|
||||
objectName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type TransientTokenOutput = {
|
||||
__typename?: 'TransientTokenOutput';
|
||||
transientToken: AuthToken;
|
||||
@@ -4660,8 +4675,10 @@ export type UpdateServerlessFunctionInputUpdates = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
handlerName?: InputMaybe<Scalars['String']>;
|
||||
handlerPath?: InputMaybe<Scalars['String']>;
|
||||
isTool?: InputMaybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
timeoutSeconds?: InputMaybe<Scalars['Float']>;
|
||||
toolInputSchema?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
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<Scalars['String']>;
|
||||
@@ -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<string>, 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<Array<string>> | 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<string>, 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<string>, 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<Array<string>> | 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<string>, 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<string>, 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<Array<string>> | 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<string>, 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<string>, 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<Array<string>> | 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<string>, 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<typeof useGetChatThreadsQuery>;
|
||||
export type GetChatThreadsLazyQueryHookResult = ReturnType<typeof useGetChatThreadsLazyQuery>;
|
||||
export type GetChatThreadsQueryResult = Apollo.QueryResult<GetChatThreadsQuery, GetChatThreadsQueryVariables>;
|
||||
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<GetToolIndexQuery, GetToolIndexQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetToolIndexQuery, GetToolIndexQueryVariables>(GetToolIndexDocument, options);
|
||||
}
|
||||
export function useGetToolIndexLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetToolIndexQuery, GetToolIndexQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetToolIndexQuery, GetToolIndexQueryVariables>(GetToolIndexDocument, options);
|
||||
}
|
||||
export type GetToolIndexQueryHookResult = ReturnType<typeof useGetToolIndexQuery>;
|
||||
export type GetToolIndexLazyQueryHookResult = ReturnType<typeof useGetToolIndexLazyQuery>;
|
||||
export type GetToolIndexQueryResult = Apollo.QueryResult<GetToolIndexQuery, GetToolIndexQueryVariables>;
|
||||
export const TrackAnalyticsDocument = gql`
|
||||
mutation TrackAnalytics($type: AnalyticsType!, $event: String, $name: String, $properties: JSON) {
|
||||
trackAnalytics(type: $type, event: $event, name: $name, properties: $properties) {
|
||||
|
||||
@@ -951,8 +951,10 @@ export type CreateServerlessFunctionInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
handlerName?: InputMaybe<Scalars['String']>;
|
||||
handlerPath?: InputMaybe<Scalars['String']>;
|
||||
isTool?: InputMaybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
timeoutSeconds?: InputMaybe<Scalars['Float']>;
|
||||
toolInputSchema?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type CreateViewFieldInput = {
|
||||
@@ -3298,6 +3300,7 @@ export type Query = {
|
||||
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
getToolIndex: Array<ToolIndexEntry>;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
listPlans: Array<BillingPlanOutput>;
|
||||
@@ -3895,6 +3898,7 @@ export type Sentry = {
|
||||
|
||||
export type ServerlessFunction = {
|
||||
__typename?: 'ServerlessFunction';
|
||||
applicationId?: Maybe<Scalars['UUID']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
cronTriggers?: Maybe<Array<CronTrigger>>;
|
||||
databaseEventTriggers?: Maybe<Array<DatabaseEventTrigger>>;
|
||||
@@ -3902,12 +3906,14 @@ export type ServerlessFunction = {
|
||||
handlerName: Scalars['String'];
|
||||
handlerPath: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
isTool: Scalars['Boolean'];
|
||||
latestVersion?: Maybe<Scalars['String']>;
|
||||
name: Scalars['String'];
|
||||
publishedVersions: Array<Scalars['String']>;
|
||||
routeTriggers?: Maybe<Array<RouteTrigger>>;
|
||||
runtime: Scalars['String'];
|
||||
timeoutSeconds: Scalars['Float'];
|
||||
toolInputSchema?: Maybe<Scalars['JSON']>;
|
||||
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<Scalars['JSON']>;
|
||||
name: Scalars['String'];
|
||||
objectName?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type TransientTokenOutput = {
|
||||
__typename?: 'TransientTokenOutput';
|
||||
transientToken: AuthToken;
|
||||
@@ -4444,8 +4459,10 @@ export type UpdateServerlessFunctionInputUpdates = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
handlerName?: InputMaybe<Scalars['String']>;
|
||||
handlerPath?: InputMaybe<Scalars['String']>;
|
||||
isTool?: InputMaybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
timeoutSeconds?: InputMaybe<Scalars['Float']>;
|
||||
toolInputSchema?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type UpdateViewFieldInput = {
|
||||
|
||||
+37
-39
@@ -57,6 +57,40 @@ const InitialLoadingIndicator = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const MessagePartRenderer = ({ part }: { part: ExtendedUIMessagePart }) => {
|
||||
switch (part.type) {
|
||||
case 'reasoning':
|
||||
return (
|
||||
<ReasoningSummaryDisplay
|
||||
content={part.text}
|
||||
isThinking={part.state === 'streaming'}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
return <LazyMarkdownRenderer text={part.text} />;
|
||||
case 'data-routing-status':
|
||||
return <RoutingStatusDisplay data={part.data} />;
|
||||
case 'data-code-execution':
|
||||
return (
|
||||
<CodeExecutionDisplay
|
||||
code={part.data.code}
|
||||
stdout={part.data.stdout}
|
||||
stderr={part.data.stderr}
|
||||
exitCode={part.data.exitCode}
|
||||
files={part.data.files}
|
||||
isRunning={
|
||||
part.data.state === 'running' || part.data.state === 'pending'
|
||||
}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
if (isToolUIPart(part)) {
|
||||
return <ToolStepRenderer toolPart={part} />;
|
||||
}
|
||||
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 (
|
||||
<ReasoningSummaryDisplay
|
||||
key={index}
|
||||
content={part.text}
|
||||
isThinking={part.state === 'streaming'}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
return <LazyMarkdownRenderer key={index} text={part.text} />;
|
||||
case 'data-routing-status':
|
||||
return <RoutingStatusDisplay data={part.data} key={index} />;
|
||||
case 'data-code-execution':
|
||||
return (
|
||||
<CodeExecutionDisplay
|
||||
key={index}
|
||||
code={part.data.code}
|
||||
stdout={part.data.stdout}
|
||||
stderr={part.data.stderr}
|
||||
exitCode={part.data.exitCode}
|
||||
files={part.data.files}
|
||||
isRunning={
|
||||
part.data.state === 'running' || part.data.state === 'pending'
|
||||
}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
{
|
||||
if (isToolUIPart(part)) {
|
||||
return <ToolStepRenderer key={index} toolPart={part} />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (!filteredParts.length && !hasError) {
|
||||
return <InitialLoadingIndicator />;
|
||||
}
|
||||
@@ -120,7 +116,9 @@ export const AIChatAssistantMessageRenderer = ({
|
||||
return (
|
||||
<div>
|
||||
<StyledMessagePartsContainer>
|
||||
{filteredParts.map(renderMessagePart)}
|
||||
{filteredParts.map((part, index) => (
|
||||
<MessagePartRenderer key={index} part={part} />
|
||||
))}
|
||||
</StyledMessagePartsContainer>
|
||||
{isLastMessageStreaming && !hasError && <StyledStreamingIndicator />}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_TOOL_INDEX = gql`
|
||||
query GetToolIndex {
|
||||
getToolIndex {
|
||||
name
|
||||
description
|
||||
category
|
||||
objectName
|
||||
inputSchema
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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<GetToolIndexQuery>(GET_TOOL_INDEX, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
toolIndex: data?.getToolIndex ?? [],
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -454,6 +454,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.AISkillDetail}
|
||||
element={<SettingsSkillForm mode="edit" />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ServerlessFunctionDetail}
|
||||
element={<SettingsServerlessFunctionDetail />}
|
||||
/>
|
||||
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
|
||||
<Route path={SettingsPath.Domain} element={<SettingsDomain />} />
|
||||
<Route
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getToolInputSchemaFromSourceCode = async (
|
||||
sourceCode: string,
|
||||
): Promise<object | null> => {
|
||||
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;
|
||||
};
|
||||
+36
@@ -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 (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="serverless-function-name-input"
|
||||
sizeVariant="md"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={t`Function name`}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
);
|
||||
};
|
||||
+12
-6
@@ -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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
+1
-2
@@ -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 = ({
|
||||
<SettingsServerlessFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
readonly
|
||||
/>
|
||||
<SettingsServerlessFunctionTabEnvironmentVariablesSection />
|
||||
</>
|
||||
|
||||
+29
@@ -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 (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Triggers`}
|
||||
description={t`Configure when this function should be executed`}
|
||||
/>
|
||||
<StyledEmptyState>
|
||||
{t`No triggers configured for this function.`}
|
||||
</StyledEmptyState>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{databaseEvents.length > 0 && (
|
||||
|
||||
+3
@@ -31,6 +31,9 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
toolInputSchema
|
||||
isTool
|
||||
applicationId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
+10
-6
@@ -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,
|
||||
|
||||
+18
-10
@@ -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) {
|
||||
|
||||
+8
-10
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`AI`}
|
||||
@@ -21,9 +54,18 @@ export const SettingsAI = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsAIRouterSettings />
|
||||
<SettingsSkillsTable />
|
||||
<SettingsAIMCP />
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID}
|
||||
/>
|
||||
{isSkillsTab && <SettingsSkillsTable />}
|
||||
{isToolsTab && <SettingsToolsTable />}
|
||||
{isSettingsTab && (
|
||||
<>
|
||||
<SettingsAIRouterSettings />
|
||||
<SettingsAIMCP />
|
||||
</>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<SettingsAgentRoleTab
|
||||
formValues={formValues}
|
||||
onFieldChange={handleFieldChange}
|
||||
disabled={isReadonlyMode || (isEditMode ? !agent?.isCustom : false)}
|
||||
agentId={agentId}
|
||||
agentLabel={formValues.label}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<SettingsAgentSettingsTab
|
||||
formValues={formValues}
|
||||
onFieldChange={handleFieldChange}
|
||||
disabled={isReadonlyMode || (isEditMode ? !agent?.isCustom : false)}
|
||||
agent={agent}
|
||||
/>
|
||||
);
|
||||
|
||||
case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.EVALS:
|
||||
return (
|
||||
<SettingsAgentEvalsTab
|
||||
agentId={agentId}
|
||||
evaluationInputs={formValues.evaluationInputs}
|
||||
onEvaluationInputsChange={(inputs) =>
|
||||
handleFieldChange('evaluationInputs', inputs)
|
||||
}
|
||||
disabled={
|
||||
process.env.NODE_ENV === 'development'
|
||||
? isReadonlyMode
|
||||
: isReadonlyMode || (isEditMode ? !agent?.isCustom : false)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.LOGS:
|
||||
return <SettingsAgentLogsTab agentId={agentId} />;
|
||||
|
||||
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}
|
||||
/>
|
||||
<StyledContentContainer>
|
||||
{renderActiveTabContent()}
|
||||
{isRoleTab && (
|
||||
<SettingsAgentRoleTab
|
||||
formValues={formValues}
|
||||
onFieldChange={handleFieldChange}
|
||||
disabled={isFormDisabled}
|
||||
agentId={agentId}
|
||||
agentLabel={formValues.label}
|
||||
/>
|
||||
)}
|
||||
{isSettingsTab && (
|
||||
<SettingsAgentSettingsTab
|
||||
formValues={formValues}
|
||||
onFieldChange={handleFieldChange}
|
||||
disabled={isFormDisabled}
|
||||
agent={agent}
|
||||
/>
|
||||
)}
|
||||
{isEvalsTab && (
|
||||
<SettingsAgentEvalsTab
|
||||
agentId={agentId}
|
||||
evaluationInputs={formValues.evaluationInputs}
|
||||
onEvaluationInputsChange={(inputs) =>
|
||||
handleFieldChange('evaluationInputs', inputs)
|
||||
}
|
||||
disabled={isEvalsDisabled}
|
||||
/>
|
||||
)}
|
||||
{isLogsTab && <SettingsAgentLogsTab agentId={agentId} />}
|
||||
</StyledContentContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="skill-label-input"
|
||||
disabled={isReadonlyMode}
|
||||
sizeVariant="md"
|
||||
value={formValues.label}
|
||||
onChange={(value) => handleFieldChange('label', value)}
|
||||
placeholder={t`Skill name`}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
);
|
||||
};
|
||||
const title = isCreateMode ? (
|
||||
t`New Skill`
|
||||
) : loading ? (
|
||||
t`Skill`
|
||||
) : (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="skill-label-input"
|
||||
disabled={isReadonlyMode}
|
||||
sizeVariant="md"
|
||||
value={formValues.label}
|
||||
onChange={(value) => handleFieldChange('label', value)}
|
||||
placeholder={t`Skill name`}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={renderTitle()}
|
||||
title={title}
|
||||
actionButton={
|
||||
isCreateMode ? (
|
||||
<SaveAndCancelButtons
|
||||
|
||||
@@ -16,15 +16,13 @@ import { useTheme } from '@emotion/react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
H2Title,
|
||||
IconArchive,
|
||||
IconChevronRight,
|
||||
IconFilterCog,
|
||||
IconFilter,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, LightIconButton } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { MenuItemToggle, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
|
||||
import {
|
||||
@@ -40,29 +38,27 @@ import {
|
||||
StyledSkillTableRow,
|
||||
} from './SettingsSkillTableRow';
|
||||
|
||||
const StyledSearchContainer = styled.div`
|
||||
const StyledSearchAndFilterContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => 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 (
|
||||
<Section>
|
||||
<StyledHeaderContainer>
|
||||
<H2Title
|
||||
title={t`Skills`}
|
||||
description={t`Skills available in the chat`}
|
||||
/>
|
||||
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewSkill)}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`New Skill`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledHeaderContainer>
|
||||
|
||||
<StyledSearchContainer>
|
||||
<>
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="skill-table-search"
|
||||
LeftIcon={IconSearch}
|
||||
@@ -150,11 +131,15 @@ export const SettingsSkillsTable = () => {
|
||||
/>
|
||||
<Dropdown
|
||||
dropdownId="settings-skills-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
aria-label={t`Filter skills`}
|
||||
Icon={IconFilterCog}
|
||||
accent="tertiary"
|
||||
<Button
|
||||
Icon={IconFilter}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
ariaLabel={t`Filter`}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
@@ -171,9 +156,9 @@ export const SettingsSkillsTable = () => {
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
</StyledSearchAndFilterContainer>
|
||||
|
||||
<StyledTable>
|
||||
<Table>
|
||||
<StyledTableHeaderRow>
|
||||
{SETTINGS_SKILL_TABLE_METADATA.fields.map(
|
||||
(settingsSkillTableMetadataField) => (
|
||||
@@ -221,7 +206,18 @@ export const SettingsSkillsTable = () => {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledTable>
|
||||
</Section>
|
||||
</Table>
|
||||
|
||||
<StyledFooterContainer>
|
||||
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewSkill)}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`New Skill`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledFooterContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconCode,
|
||||
IconDatabase,
|
||||
IconPlayerPlay,
|
||||
IconSettings,
|
||||
IconTable,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { JsonTree } from 'twenty-ui/json-visualizer';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
export type SystemTool = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
objectName?: string;
|
||||
inputSchema?: object;
|
||||
};
|
||||
|
||||
export type SettingsSystemToolTableRowProps = {
|
||||
tool: SystemTool;
|
||||
};
|
||||
|
||||
export const StyledSystemToolTableRow = styled(TableRow)<{
|
||||
isExpandable?: boolean;
|
||||
}>`
|
||||
grid-template-columns: 1fr 100px 36px;
|
||||
opacity: 0.7;
|
||||
cursor: ${({ isExpandable }) => (isExpandable ? 'pointer' : 'default')};
|
||||
|
||||
&:hover {
|
||||
opacity: ${({ isExpandable }) => (isExpandable ? 0.85 : 0.7)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledActionTableCell = styled(TableCell)`
|
||||
justify-content: flex-end;
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledExpandableContent = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category) {
|
||||
case 'database':
|
||||
return IconDatabase;
|
||||
case 'workflow':
|
||||
return IconPlayerPlay;
|
||||
case 'metadata':
|
||||
return IconSettings;
|
||||
case 'view':
|
||||
return IconTable;
|
||||
default:
|
||||
return IconCode;
|
||||
}
|
||||
};
|
||||
|
||||
export const SettingsSystemToolTableRow = ({
|
||||
tool,
|
||||
}: SettingsSystemToolTableRowProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const Icon = getCategoryIcon(tool.category);
|
||||
const hasInputSchema =
|
||||
isDefined(tool.inputSchema) && Object.keys(tool.inputSchema).length > 0;
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (hasInputSchema) {
|
||||
setIsExpanded(!isExpanded);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledSystemToolTableRow
|
||||
key={tool.name}
|
||||
onClick={handleRowClick}
|
||||
isExpandable={hasInputSchema}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
<StyledIconContainer>
|
||||
<Icon size={16} />
|
||||
</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={tool.name} />
|
||||
</StyledNameTableCell>
|
||||
<TableCell>
|
||||
<SettingsItemTypeTag item={{ isCustom: false }} />
|
||||
</TableCell>
|
||||
<StyledActionTableCell>
|
||||
{hasInputSchema &&
|
||||
(isExpanded ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
))}
|
||||
</StyledActionTableCell>
|
||||
</StyledSystemToolTableRow>
|
||||
|
||||
{hasInputSchema && (
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
||||
<StyledExpandableContent>
|
||||
{tool.description && (
|
||||
<StyledDescription>{tool.description}</StyledDescription>
|
||||
)}
|
||||
<StyledSectionTitle>{t`Input Schema`}</StyledSectionTitle>
|
||||
<JsonTree
|
||||
value={tool.inputSchema as JsonValue}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`No parameters`}
|
||||
emptyStringLabel={t`[empty string]`}
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
onNodeValueClick={copyToClipboard}
|
||||
/>
|
||||
</StyledExpandableContent>
|
||||
</AnimatedExpandableContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { IconCode, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
import { type ServerlessFunction } from '~/generated-metadata/graphql';
|
||||
|
||||
type ToolWithApplicationId = ServerlessFunction & {
|
||||
applicationId?: string | null;
|
||||
};
|
||||
|
||||
export type SettingsToolTableRowProps = {
|
||||
tool: ToolWithApplicationId;
|
||||
action?: ReactNode;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export const StyledToolTableRow = styled(TableRow)`
|
||||
grid-template-columns: 1fr 100px 36px;
|
||||
`;
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledActionTableCell = styled(TableCell)`
|
||||
justify-content: flex-end;
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsToolTableRow = ({
|
||||
tool,
|
||||
action,
|
||||
link,
|
||||
}: SettingsToolTableRowProps) => {
|
||||
return (
|
||||
<StyledToolTableRow to={link}>
|
||||
<StyledNameTableCell>
|
||||
<StyledIconContainer>
|
||||
<IconCode size={16} />
|
||||
</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={tool.name} />
|
||||
</StyledNameTableCell>
|
||||
<TableCell>
|
||||
<SettingsItemTypeTag
|
||||
item={{ isCustom: true, applicationId: tool.applicationId }}
|
||||
/>
|
||||
</TableCell>
|
||||
<StyledActionTableCell>{action}</StyledActionTableCell>
|
||||
</StyledToolTableRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { usePersistServerlessFunction } from '@/settings/serverless-functions/hooks/usePersistServerlessFunction';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
import { SettingsSystemToolTableRow } from './SettingsSystemToolTableRow';
|
||||
import {
|
||||
SettingsToolTableRow,
|
||||
StyledToolTableRow,
|
||||
} from './SettingsToolTableRow';
|
||||
|
||||
const StyledSearchAndFilterContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
align-items: center;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTableHeaderRow = styled(StyledToolTableRow)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledFooterContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const DEFAULT_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
};
|
||||
|
||||
export const SettingsToolsTable = () => {
|
||||
const { serverlessFunctions, loading } = useGetManyServerlessFunctions();
|
||||
const { toolIndex, loading: toolIndexLoading } = useGetToolIndex();
|
||||
const { createServerlessFunction } = usePersistServerlessFunction();
|
||||
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const [customSearchTerm, setCustomSearchTerm] = useState('');
|
||||
const [builtInSearchTerm, setBuiltInSearchTerm] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
// Filter to only show serverless functions that are marked as tools
|
||||
const tools = useMemo(
|
||||
() => serverlessFunctions.filter((fn) => fn.isTool === true),
|
||||
[serverlessFunctions],
|
||||
);
|
||||
|
||||
const filteredTools = useMemo(
|
||||
() =>
|
||||
tools.filter((tool) => {
|
||||
const searchNormalized = normalizeSearchText(customSearchTerm);
|
||||
const matchesSearch =
|
||||
normalizeSearchText(tool.name).includes(searchNormalized) ||
|
||||
normalizeSearchText(tool.description ?? '').includes(
|
||||
searchNormalized,
|
||||
);
|
||||
|
||||
return matchesSearch;
|
||||
}),
|
||||
[tools, customSearchTerm],
|
||||
);
|
||||
|
||||
// System tools from the tool index (excluding serverless function tools which are shown separately)
|
||||
const systemTools = useMemo(
|
||||
() =>
|
||||
toolIndex.filter(
|
||||
(systemTool) => systemTool.category !== 'SERVERLESS_FUNCTION',
|
||||
),
|
||||
[toolIndex],
|
||||
);
|
||||
|
||||
const filteredSystemTools = useMemo(
|
||||
() =>
|
||||
systemTools.filter((systemTool) => {
|
||||
const searchNormalized = normalizeSearchText(builtInSearchTerm);
|
||||
const matchesSearch =
|
||||
normalizeSearchText(systemTool.name).includes(searchNormalized) ||
|
||||
normalizeSearchText(systemTool.description).includes(
|
||||
searchNormalized,
|
||||
);
|
||||
|
||||
return matchesSearch;
|
||||
}),
|
||||
[systemTools, builtInSearchTerm],
|
||||
);
|
||||
|
||||
const showSkeleton = (loading || toolIndexLoading) && tools.length === 0;
|
||||
|
||||
const handleCreateTool = async () => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const result = await createServerlessFunction({
|
||||
input: {
|
||||
name: 'new-tool',
|
||||
isTool: true,
|
||||
toolInputSchema: DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 'successful' && isDefined(result.response?.data)) {
|
||||
const newFunction = result.response.data.createOneServerlessFunction;
|
||||
enqueueSuccessSnackBar({ message: t`Tool created` });
|
||||
|
||||
// Navigate to the serverless function detail page
|
||||
// The applicationId might be null for workspace-level functions
|
||||
const applicationId = (newFunction as { applicationId?: string })
|
||||
.applicationId;
|
||||
if (isDefined(applicationId)) {
|
||||
navigate(
|
||||
getSettingsPath(SettingsPath.ApplicationServerlessFunctionDetail, {
|
||||
applicationId,
|
||||
serverlessFunctionId: newFunction.id,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
navigate(
|
||||
getSettingsPath(SettingsPath.ServerlessFunctionDetail, {
|
||||
serverlessFunctionId: newFunction.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: t`Failed to create tool` });
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getToolLink = (tool: (typeof tools)[0]) => {
|
||||
const applicationId = (tool as { applicationId?: string }).applicationId;
|
||||
if (isDefined(applicationId)) {
|
||||
return getSettingsPath(SettingsPath.ApplicationServerlessFunctionDetail, {
|
||||
applicationId,
|
||||
serverlessFunctionId: tool.id,
|
||||
});
|
||||
}
|
||||
return getSettingsPath(SettingsPath.ServerlessFunctionDetail, {
|
||||
serverlessFunctionId: tool.id,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Custom`}
|
||||
description={t`Custom tools created in your workspace`}
|
||||
/>
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="custom-tool-table-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a custom tool...`}
|
||||
value={customSearchTerm}
|
||||
onChange={setCustomSearchTerm}
|
||||
/>
|
||||
</StyledSearchAndFilterContainer>
|
||||
<Table>
|
||||
<StyledTableHeaderRow>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Type`}</TableHeader>
|
||||
<TableHeader />
|
||||
</StyledTableHeaderRow>
|
||||
{showSkeleton
|
||||
? Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton height={32} borderRadius={4} key={index} />
|
||||
))
|
||||
: filteredTools.map((tool) => (
|
||||
<SettingsToolTableRow
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
action={
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
link={getToolLink(tool)}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
|
||||
<StyledFooterContainer>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`New Tool`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={handleCreateTool}
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</StyledFooterContainer>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Built-in`}
|
||||
description={t`Standard tools available to AI agents`}
|
||||
/>
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="builtin-tool-table-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a built-in tool...`}
|
||||
value={builtInSearchTerm}
|
||||
onChange={setBuiltInSearchTerm}
|
||||
/>
|
||||
</StyledSearchAndFilterContainer>
|
||||
<Table>
|
||||
<StyledTableHeaderRow>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Type`}</TableHeader>
|
||||
<TableHeader />
|
||||
</StyledTableHeaderRow>
|
||||
{filteredSystemTools.map((systemTool) => (
|
||||
<SettingsSystemToolTableRow
|
||||
key={systemTool.name}
|
||||
tool={systemTool}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
+96
-56
@@ -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 (
|
||||
<SettingsServerlessFunctionCodeEditorTab
|
||||
files={files}
|
||||
handleExecute={handleTestFunction}
|
||||
onChange={onCodeChange}
|
||||
isTesting={isTesting}
|
||||
/>
|
||||
);
|
||||
case 'triggers':
|
||||
return serverlessFunction ? (
|
||||
<SettingsServerlessFunctionTriggersTab
|
||||
serverlessFunction={serverlessFunction}
|
||||
/>
|
||||
) : null;
|
||||
case 'test':
|
||||
return (
|
||||
<SettingsServerlessFunctionTestTab
|
||||
serverlessFunctionId={serverlessFunctionId}
|
||||
handleExecute={handleTestFunction}
|
||||
isTesting={isTesting}
|
||||
/>
|
||||
);
|
||||
case 'settings':
|
||||
return (
|
||||
<SettingsServerlessFunctionSettingsTab
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
const isEditorTab = activeTabId === 'editor';
|
||||
const isTriggersTab = activeTabId === 'triggers';
|
||||
const isTestTab = activeTabId === 'test';
|
||||
const isSettingsTab = activeTabId === 'settings';
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<SubMenuTopBarContainer
|
||||
title={formValues.name}
|
||||
links={[
|
||||
const breadcrumbLinks =
|
||||
isDefined(applicationId) && applicationId !== ''
|
||||
? [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
@@ -181,11 +173,59 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
),
|
||||
},
|
||||
{ children: `${serverlessFunction?.name}` },
|
||||
]}
|
||||
]
|
||||
: [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
href: getSettingsPath(SettingsPath.AI),
|
||||
},
|
||||
{ children: `${serverlessFunction?.name}` },
|
||||
];
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<SettingsServerlessFunctionLabelContainer
|
||||
value={formValues.name}
|
||||
onChange={onChange('name')}
|
||||
/>
|
||||
}
|
||||
links={breadcrumbLinks}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList tabs={tabs} componentInstanceId={instanceId} />
|
||||
{renderActiveTabContent()}
|
||||
{isEditorTab && (
|
||||
<SettingsServerlessFunctionCodeEditorTab
|
||||
files={files}
|
||||
handleExecute={handleTestFunction}
|
||||
onChange={onCodeChange}
|
||||
isTesting={isTesting}
|
||||
isManaged={isManaged}
|
||||
/>
|
||||
)}
|
||||
{isTriggersTab && serverlessFunction && (
|
||||
<SettingsServerlessFunctionTriggersTab
|
||||
serverlessFunction={serverlessFunction}
|
||||
/>
|
||||
)}
|
||||
{isTestTab && (
|
||||
<SettingsServerlessFunctionTestTab
|
||||
serverlessFunctionId={serverlessFunctionId}
|
||||
handleExecute={handleTestFunction}
|
||||
isTesting={isTesting}
|
||||
/>
|
||||
)}
|
||||
{isSettingsTab && (
|
||||
<SettingsServerlessFunctionSettingsTab
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
)
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddToolSchemaToServerlessFunction1767364430164
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddToolSchemaToServerlessFunction1767364430164';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."serverlessFunction" DROP COLUMN "isTool"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."serverlessFunction" DROP COLUMN "toolInputSchema"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -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<FeatureFlagService>;
|
||||
let toolProviderService: jest.Mocked<ToolProviderService>;
|
||||
let toolRegistryService: jest.Mocked<ToolRegistryService>;
|
||||
let userRoleService: jest.Mocked<UserRoleService>;
|
||||
let mcpToolExecutorService: jest.Mocked<McpToolExecutorService>;
|
||||
let apiKeyRoleService: jest.Mocked<ApiKeyRoleService>;
|
||||
@@ -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>(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(
|
||||
|
||||
@@ -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<ToolSet> {
|
||||
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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
+13
-5
@@ -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,
|
||||
|
||||
+1
-12
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+1
-11
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+11
-19
@@ -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<typeof BulkDeleteToolInputSchema>;
|
||||
|
||||
+1
-9
@@ -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<typeof FindOneToolInputSchema>;
|
||||
|
||||
+27
-35
@@ -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'),
|
||||
});
|
||||
};
|
||||
|
||||
+3
-11
@@ -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<typeof SoftDeleteToolInputSchema>;
|
||||
|
||||
+1
@@ -6,4 +6,5 @@ export enum ToolCategory {
|
||||
NATIVE_MODEL = 'NATIVE_MODEL',
|
||||
VIEW = 'VIEW',
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
SERVERLESS_FUNCTION = 'SERVERLESS_FUNCTION',
|
||||
}
|
||||
|
||||
+15
@@ -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;
|
||||
|
||||
|
||||
+10
-3
@@ -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<ZodRawShape>,
|
||||
),
|
||||
execute: async (parameters: ToolInput) =>
|
||||
tool.execute(stripLoadingMessage(parameters), context),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -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<boolean> {
|
||||
return isDefined(context.agent);
|
||||
}
|
||||
|
||||
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
|
||||
if (!context.agent) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(context.agent);
|
||||
|
||||
return this.agentModelConfigService.getNativeModelTools(
|
||||
registeredModel,
|
||||
context.agent,
|
||||
);
|
||||
}
|
||||
}
|
||||
+100
@@ -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<boolean> {
|
||||
// Serverless function tools are available if there are any functions marked as tools
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
|
||||
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<string, unknown>,
|
||||
);
|
||||
|
||||
tools[toolName] = {
|
||||
description:
|
||||
serverlessFunction.description ||
|
||||
`Execute the ${serverlessFunction.name} serverless function`,
|
||||
inputSchema: jsonSchema(wrappedSchema),
|
||||
execute: async (parameters: Record<string, unknown>) => {
|
||||
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, '')}`;
|
||||
}
|
||||
}
|
||||
+59
@@ -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<ToolIndexEntryDTO[]> {
|
||||
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!roleId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.toolRegistryService.buildToolIndex(workspace.id, roleId, {
|
||||
userWorkspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
-352
@@ -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<ToolType, ActionTool>;
|
||||
|
||||
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<ToolSet> {
|
||||
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<ToolSet> {
|
||||
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<ToolSet> {
|
||||
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<ToolSet> {
|
||||
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<ToolSet> {
|
||||
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<ToolSet> {
|
||||
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<ToolSet> {
|
||||
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<typeof originalExecute>) => {
|
||||
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';
|
||||
}
|
||||
}
|
||||
+225
-25
@@ -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<ToolSet> {
|
||||
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<ToolCategory, ToolIndexEntry['category']> = {
|
||||
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<string, unknown>;
|
||||
|
||||
// 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<string, unknown>;
|
||||
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<typeof originalExecute>) => {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -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 {}
|
||||
|
||||
+8
-12
@@ -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<typeof loadSkillInputSchema>['input'];
|
||||
export type LoadSkillInput = z.infer<typeof loadSkillInputSchema>;
|
||||
|
||||
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<LoadSkillResult> => {
|
||||
const { skillNames } = parameters.input;
|
||||
execute: async (parameters: LoadSkillInput): Promise<LoadSkillResult> => {
|
||||
const { skillNames } = parameters;
|
||||
|
||||
const skills = await loadSkills(skillNames);
|
||||
|
||||
|
||||
+8
-12
@@ -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<typeof loadToolsInputSchema>['input'];
|
||||
export type LoadToolsInput = z.infer<typeof loadToolsInputSchema>;
|
||||
|
||||
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<LoadToolsResult> => {
|
||||
const { toolNames } = parameters.input;
|
||||
execute: async (parameters: LoadToolsInput): Promise<LoadToolsResult> => {
|
||||
const { toolNames } = parameters;
|
||||
|
||||
const loaded: string[] = [];
|
||||
const notFound: string[] = [];
|
||||
|
||||
-20
@@ -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[];
|
||||
};
|
||||
-9
@@ -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,
|
||||
});
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
-9
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
-9
@@ -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
|
||||
>;
|
||||
|
||||
+2
-2
@@ -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) {}
|
||||
|
||||
|
||||
+1
-10
@@ -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,
|
||||
});
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+48
@@ -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 = <T extends z.ZodRawShape>(
|
||||
schema: z.ZodObject<T>,
|
||||
customLoadingMessageSchema?: z.ZodString,
|
||||
): z.ZodObject<T & { loadingMessage: z.ZodString }> => {
|
||||
return z.object({
|
||||
loadingMessage:
|
||||
customLoadingMessageSchema ?? DEFAULT_LOADING_MESSAGE_SCHEMA,
|
||||
...schema.shape,
|
||||
}) as z.ZodObject<T & { loadingMessage: z.ZodString }>;
|
||||
};
|
||||
|
||||
// For non-Zod schemas (serverless functions with JSON Schema)
|
||||
export const wrapJsonSchemaForExecution = (
|
||||
schema: Record<string, unknown>,
|
||||
): Record<string, unknown> => {
|
||||
const properties = (schema.properties as Record<string, unknown>) ?? {};
|
||||
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 = <T extends Record<string, unknown>>(
|
||||
parameters: T,
|
||||
): Omit<T, 'loadingMessage'> => {
|
||||
const { loadingMessage: _, ...rest } = parameters;
|
||||
|
||||
return rest;
|
||||
};
|
||||
+25
-17
@@ -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<RoleTargetEntity>,
|
||||
) {}
|
||||
@@ -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,
|
||||
|
||||
+15
-12
@@ -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;
|
||||
}
|
||||
|
||||
+115
-155
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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],
|
||||
|
||||
+83
-127
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+9
@@ -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' },
|
||||
},
|
||||
};
|
||||
+2
@@ -8,4 +8,6 @@ export const FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES = [
|
||||
'code',
|
||||
'handlerPath',
|
||||
'handlerName',
|
||||
'toolInputSchema',
|
||||
'isTool',
|
||||
] as const satisfies (keyof FlatServerlessFunction)[];
|
||||
|
||||
+11
@@ -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;
|
||||
}
|
||||
|
||||
+18
@@ -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;
|
||||
|
||||
|
||||
+11
@@ -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()
|
||||
|
||||
+6
@@ -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;
|
||||
|
||||
|
||||
+17
-8
@@ -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 =
|
||||
|
||||
+12
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+18
-27
@@ -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');
|
||||
});
|
||||
|
||||
+67
-101
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, InputJsonSchema>;
|
||||
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 = {
|
||||
|
||||
@@ -7,5 +7,4 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
|
||||
export default {};
|
||||
|
||||
Reference in New Issue
Block a user