feat: Add AI Agent workflow action node (#12650)
https://github.com/user-attachments/assets/8593e488-cb00-4fd2-b903-5ba5766e0254 --------- Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Baptiste Devessier <baptiste@devessier.fr> Co-authored-by: Joseph Chiang <josephj6802@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Guillim <guillim@users.noreply.github.com> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com> Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: Ajay A Adsule <103304466+AjayAdsule@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Marty <91310557+real-marty@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
This commit is contained in:
@ -57,6 +57,23 @@ export type AdminPanelWorkerQueueHealth = {
|
|||||||
status: AdminPanelHealthServiceStatus;
|
status: AdminPanelHealthServiceStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Agent = {
|
||||||
|
__typename?: 'Agent';
|
||||||
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
description?: Maybe<Scalars['String']['output']>;
|
||||||
|
id: Scalars['UUID']['output'];
|
||||||
|
modelId: Scalars['String']['output'];
|
||||||
|
name: Scalars['String']['output'];
|
||||||
|
prompt: Scalars['String']['output'];
|
||||||
|
responseFormat?: Maybe<Scalars['JSON']['output']>;
|
||||||
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentIdInput = {
|
||||||
|
/** The id of the agent. */
|
||||||
|
id: Scalars['UUID']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
__typename?: 'Analytics';
|
__typename?: 'Analytics';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -320,8 +337,18 @@ export type CheckUserExistOutput = {
|
|||||||
isEmailVerified: Scalars['Boolean']['output'];
|
isEmailVerified: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClientAiModelConfig = {
|
||||||
|
__typename?: 'ClientAIModelConfig';
|
||||||
|
inputCostPer1kTokensInCredits: Scalars['Float']['output'];
|
||||||
|
label: Scalars['String']['output'];
|
||||||
|
modelId: Scalars['String']['output'];
|
||||||
|
outputCostPer1kTokensInCredits: Scalars['Float']['output'];
|
||||||
|
provider: ModelProvider;
|
||||||
|
};
|
||||||
|
|
||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
|
aiModels: Array<ClientAiModelConfig>;
|
||||||
analyticsEnabled: Scalars['Boolean']['output'];
|
analyticsEnabled: Scalars['Boolean']['output'];
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
@ -413,6 +440,14 @@ export type ConfigVariablesOutput = {
|
|||||||
groups: Array<ConfigVariablesGroupData>;
|
groups: Array<ConfigVariablesGroupData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateAgentInput = {
|
||||||
|
description?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
modelId: Scalars['String']['input'];
|
||||||
|
name: Scalars['String']['input'];
|
||||||
|
prompt: Scalars['String']['input'];
|
||||||
|
responseFormat?: InputMaybe<Scalars['JSON']['input']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateAppTokenInput = {
|
export type CreateAppTokenInput = {
|
||||||
expiresAt: Scalars['DateTime']['input'];
|
expiresAt: Scalars['DateTime']['input'];
|
||||||
};
|
};
|
||||||
@ -944,6 +979,11 @@ export enum MessageChannelVisibility {
|
|||||||
SUBJECT = 'SUBJECT'
|
SUBJECT = 'SUBJECT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ModelProvider {
|
||||||
|
ANTHROPIC = 'ANTHROPIC',
|
||||||
|
OPENAI = 'OPENAI'
|
||||||
|
}
|
||||||
|
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
activateWorkflowVersion: Scalars['Boolean']['output'];
|
activateWorkflowVersion: Scalars['Boolean']['output'];
|
||||||
@ -957,6 +997,7 @@ export type Mutation = {
|
|||||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||||
createOIDCIdentityProvider: SetupSsoOutput;
|
createOIDCIdentityProvider: SetupSsoOutput;
|
||||||
createObjectEvent: Analytics;
|
createObjectEvent: Analytics;
|
||||||
|
createOneAgent: Agent;
|
||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
@ -969,6 +1010,7 @@ export type Mutation = {
|
|||||||
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
|
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
deleteDatabaseConfigVariable: Scalars['Boolean']['output'];
|
deleteDatabaseConfigVariable: Scalars['Boolean']['output'];
|
||||||
|
deleteOneAgent: Agent;
|
||||||
deleteOneField: Field;
|
deleteOneField: Field;
|
||||||
deleteOneObject: Object;
|
deleteOneObject: Object;
|
||||||
deleteOneRemoteServer: RemoteServer;
|
deleteOneRemoteServer: RemoteServer;
|
||||||
@ -1012,6 +1054,7 @@ export type Mutation = {
|
|||||||
unsyncRemoteTable: RemoteTable;
|
unsyncRemoteTable: RemoteTable;
|
||||||
updateDatabaseConfigVariable: Scalars['Boolean']['output'];
|
updateDatabaseConfigVariable: Scalars['Boolean']['output'];
|
||||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||||
|
updateOneAgent: Agent;
|
||||||
updateOneField: Field;
|
updateOneField: Field;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
updateOneRemoteServer: RemoteServer;
|
updateOneRemoteServer: RemoteServer;
|
||||||
@ -1093,6 +1136,11 @@ export type MutationCreateObjectEventArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOneAgentArgs = {
|
||||||
|
input: CreateAgentInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneAppTokenArgs = {
|
export type MutationCreateOneAppTokenArgs = {
|
||||||
input: CreateOneAppTokenInput;
|
input: CreateOneAppTokenInput;
|
||||||
};
|
};
|
||||||
@ -1148,6 +1196,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteOneAgentArgs = {
|
||||||
|
input: AgentIdInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteOneFieldArgs = {
|
export type MutationDeleteOneFieldArgs = {
|
||||||
input: DeleteOneFieldInput;
|
input: DeleteOneFieldInput;
|
||||||
};
|
};
|
||||||
@ -1339,6 +1392,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateOneAgentArgs = {
|
||||||
|
input: UpdateAgentInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateOneFieldArgs = {
|
export type MutationUpdateOneFieldArgs = {
|
||||||
input: UpdateOneFieldMetadataInput;
|
input: UpdateOneFieldMetadataInput;
|
||||||
};
|
};
|
||||||
@ -1655,8 +1713,10 @@ export type Query = {
|
|||||||
field: Field;
|
field: Field;
|
||||||
fields: FieldConnection;
|
fields: FieldConnection;
|
||||||
findDistantTablesWithStatus: Array<RemoteTable>;
|
findDistantTablesWithStatus: Array<RemoteTable>;
|
||||||
|
findManyAgents: Array<Agent>;
|
||||||
findManyRemoteServersByType: Array<RemoteServer>;
|
findManyRemoteServersByType: Array<RemoteServer>;
|
||||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||||
|
findOneAgent: Agent;
|
||||||
findOneRemoteServerById: RemoteServer;
|
findOneRemoteServerById: RemoteServer;
|
||||||
findOneServerlessFunction: ServerlessFunction;
|
findOneServerlessFunction: ServerlessFunction;
|
||||||
findWorkspaceFromInviteHash: Workspace;
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
@ -1726,6 +1786,11 @@ export type QueryFindManyRemoteServersByTypeArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryFindOneAgentArgs = {
|
||||||
|
input: AgentIdInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryFindOneRemoteServerByIdArgs = {
|
export type QueryFindOneRemoteServerByIdArgs = {
|
||||||
input: RemoteServerIdInput;
|
input: RemoteServerIdInput;
|
||||||
};
|
};
|
||||||
@ -2280,6 +2345,15 @@ export type UuidFilterComparison = {
|
|||||||
notLike?: InputMaybe<Scalars['UUID']['input']>;
|
notLike?: InputMaybe<Scalars['UUID']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateAgentInput = {
|
||||||
|
description?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
id: Scalars['UUID']['input'];
|
||||||
|
modelId: Scalars['String']['input'];
|
||||||
|
name: Scalars['String']['input'];
|
||||||
|
prompt: Scalars['String']['input'];
|
||||||
|
responseFormat?: InputMaybe<Scalars['JSON']['input']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateFieldInput = {
|
export type UpdateFieldInput = {
|
||||||
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
||||||
description?: InputMaybe<Scalars['String']['input']>;
|
description?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
|||||||
@ -49,6 +49,23 @@ export type AdminPanelWorkerQueueHealth = {
|
|||||||
status: AdminPanelHealthServiceStatus;
|
status: AdminPanelHealthServiceStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Agent = {
|
||||||
|
__typename?: 'Agent';
|
||||||
|
createdAt: Scalars['DateTime'];
|
||||||
|
description?: Maybe<Scalars['String']>;
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
modelId: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
prompt: Scalars['String'];
|
||||||
|
responseFormat?: Maybe<Scalars['JSON']>;
|
||||||
|
updatedAt: Scalars['DateTime'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentIdInput = {
|
||||||
|
/** The id of the agent. */
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
__typename?: 'Analytics';
|
__typename?: 'Analytics';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -312,8 +329,18 @@ export type CheckUserExistOutput = {
|
|||||||
isEmailVerified: Scalars['Boolean'];
|
isEmailVerified: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClientAiModelConfig = {
|
||||||
|
__typename?: 'ClientAIModelConfig';
|
||||||
|
inputCostPer1kTokensInCredits: Scalars['Float'];
|
||||||
|
label: Scalars['String'];
|
||||||
|
modelId: Scalars['String'];
|
||||||
|
outputCostPer1kTokensInCredits: Scalars['Float'];
|
||||||
|
provider: ModelProvider;
|
||||||
|
};
|
||||||
|
|
||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
|
aiModels: Array<ClientAiModelConfig>;
|
||||||
analyticsEnabled: Scalars['Boolean'];
|
analyticsEnabled: Scalars['Boolean'];
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
@ -405,6 +432,14 @@ export type ConfigVariablesOutput = {
|
|||||||
groups: Array<ConfigVariablesGroupData>;
|
groups: Array<ConfigVariablesGroupData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateAgentInput = {
|
||||||
|
description?: InputMaybe<Scalars['String']>;
|
||||||
|
modelId: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
prompt: Scalars['String'];
|
||||||
|
responseFormat?: InputMaybe<Scalars['JSON']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateApprovedAccessDomainInput = {
|
export type CreateApprovedAccessDomainInput = {
|
||||||
domain: Scalars['String'];
|
domain: Scalars['String'];
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -893,6 +928,11 @@ export enum MessageChannelVisibility {
|
|||||||
SUBJECT = 'SUBJECT'
|
SUBJECT = 'SUBJECT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ModelProvider {
|
||||||
|
ANTHROPIC = 'ANTHROPIC',
|
||||||
|
OPENAI = 'OPENAI'
|
||||||
|
}
|
||||||
|
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
activateWorkflowVersion: Scalars['Boolean'];
|
activateWorkflowVersion: Scalars['Boolean'];
|
||||||
@ -906,6 +946,7 @@ export type Mutation = {
|
|||||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||||
createOIDCIdentityProvider: SetupSsoOutput;
|
createOIDCIdentityProvider: SetupSsoOutput;
|
||||||
createObjectEvent: Analytics;
|
createObjectEvent: Analytics;
|
||||||
|
createOneAgent: Agent;
|
||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
@ -917,6 +958,7 @@ export type Mutation = {
|
|||||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||||
|
deleteOneAgent: Agent;
|
||||||
deleteOneField: Field;
|
deleteOneField: Field;
|
||||||
deleteOneObject: Object;
|
deleteOneObject: Object;
|
||||||
deleteOneRole: Scalars['String'];
|
deleteOneRole: Scalars['String'];
|
||||||
@ -956,6 +998,7 @@ export type Mutation = {
|
|||||||
trackAnalytics: Analytics;
|
trackAnalytics: Analytics;
|
||||||
updateDatabaseConfigVariable: Scalars['Boolean'];
|
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||||
|
updateOneAgent: Agent;
|
||||||
updateOneField: Field;
|
updateOneField: Field;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
updateOneRole: Role;
|
updateOneRole: Role;
|
||||||
@ -1036,6 +1079,11 @@ export type MutationCreateObjectEventArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOneAgentArgs = {
|
||||||
|
input: CreateAgentInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneFieldArgs = {
|
export type MutationCreateOneFieldArgs = {
|
||||||
input: CreateOneFieldMetadataInput;
|
input: CreateOneFieldMetadataInput;
|
||||||
};
|
};
|
||||||
@ -1076,6 +1124,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteOneAgentArgs = {
|
||||||
|
input: AgentIdInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteOneFieldArgs = {
|
export type MutationDeleteOneFieldArgs = {
|
||||||
input: DeleteOneFieldInput;
|
input: DeleteOneFieldInput;
|
||||||
};
|
};
|
||||||
@ -1247,6 +1300,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateOneAgentArgs = {
|
||||||
|
input: UpdateAgentInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateOneFieldArgs = {
|
export type MutationUpdateOneFieldArgs = {
|
||||||
input: UpdateOneFieldMetadataInput;
|
input: UpdateOneFieldMetadataInput;
|
||||||
};
|
};
|
||||||
@ -1557,7 +1615,9 @@ export type Query = {
|
|||||||
currentWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
field: Field;
|
field: Field;
|
||||||
fields: FieldConnection;
|
fields: FieldConnection;
|
||||||
|
findManyAgents: Array<Agent>;
|
||||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||||
|
findOneAgent: Agent;
|
||||||
findOneServerlessFunction: ServerlessFunction;
|
findOneServerlessFunction: ServerlessFunction;
|
||||||
findWorkspaceFromInviteHash: Workspace;
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||||
@ -1605,6 +1665,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryFindOneAgentArgs = {
|
||||||
|
input: AgentIdInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryFindOneServerlessFunctionArgs = {
|
export type QueryFindOneServerlessFunctionArgs = {
|
||||||
input: ServerlessFunctionIdInput;
|
input: ServerlessFunctionIdInput;
|
||||||
};
|
};
|
||||||
@ -2118,6 +2183,15 @@ export type UuidFilterComparison = {
|
|||||||
notLike?: InputMaybe<Scalars['UUID']>;
|
notLike?: InputMaybe<Scalars['UUID']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateAgentInput = {
|
||||||
|
description?: InputMaybe<Scalars['String']>;
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
modelId: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
prompt: Scalars['String'];
|
||||||
|
responseFormat?: InputMaybe<Scalars['JSON']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateFieldInput = {
|
export type UpdateFieldInput = {
|
||||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||||
description?: InputMaybe<Scalars['String']>;
|
description?: InputMaybe<Scalars['String']>;
|
||||||
@ -2751,7 +2825,7 @@ export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredPro
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, isAttachmentPreviewEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, isMicrosoftMessagingEnabled: boolean, isMicrosoftCalendarEnabled: boolean, isGoogleMessagingEnabled: boolean, isGoogleCalendarEnabled: boolean, isConfigVariablesInDbEnabled: boolean, calendarBookingPageId?: string | null, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'BillingTrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: SupportDriver, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number }, publicFeatureFlags: Array<{ __typename?: 'PublicFeatureFlag', key: FeatureFlagKey, metadata: { __typename?: 'PublicFeatureFlagMetadata', label: string, description: string, imagePath: string } }> } };
|
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, isAttachmentPreviewEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, isMicrosoftMessagingEnabled: boolean, isMicrosoftCalendarEnabled: boolean, isGoogleMessagingEnabled: boolean, isGoogleCalendarEnabled: boolean, isConfigVariablesInDbEnabled: boolean, calendarBookingPageId?: string | null, aiModels: Array<{ __typename?: 'ClientAIModelConfig', modelId: string, label: string, provider: ModelProvider, inputCostPer1kTokensInCredits: number, outputCostPer1kTokensInCredits: number }>, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'BillingTrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: SupportDriver, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number }, publicFeatureFlags: Array<{ __typename?: 'PublicFeatureFlag', key: FeatureFlagKey, metadata: { __typename?: 'PublicFeatureFlagMetadata', label: string, description: string, imagePath: string } }> } };
|
||||||
|
|
||||||
export type SearchQueryVariables = Exact<{
|
export type SearchQueryVariables = Exact<{
|
||||||
searchInput: Scalars['String'];
|
searchInput: Scalars['String'];
|
||||||
@ -3062,6 +3136,20 @@ export type UpdateWorkflowVersionStepMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
|
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
|
||||||
|
|
||||||
|
export type UpdateOneAgentMutationVariables = Exact<{
|
||||||
|
input: UpdateAgentInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UpdateOneAgentMutation = { __typename?: 'Mutation', updateOneAgent: { __typename?: 'Agent', id: any, name: string, description?: string | null, prompt: string, modelId: string, responseFormat?: any | null } };
|
||||||
|
|
||||||
|
export type FindOneAgentQueryVariables = Exact<{
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type FindOneAgentQuery = { __typename?: 'Query', findOneAgent: { __typename?: 'Agent', id: any, name: string, description?: string | null, prompt: string, modelId: string, responseFormat?: any | null } };
|
||||||
|
|
||||||
export type SubmitFormStepMutationVariables = Exact<{
|
export type SubmitFormStepMutationVariables = Exact<{
|
||||||
input: SubmitFormStepInput;
|
input: SubmitFormStepInput;
|
||||||
}>;
|
}>;
|
||||||
@ -4708,6 +4796,13 @@ export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult<GetMeteredPr
|
|||||||
export const GetClientConfigDocument = gql`
|
export const GetClientConfigDocument = gql`
|
||||||
query GetClientConfig {
|
query GetClientConfig {
|
||||||
clientConfig {
|
clientConfig {
|
||||||
|
aiModels {
|
||||||
|
modelId
|
||||||
|
label
|
||||||
|
provider
|
||||||
|
inputCostPer1kTokensInCredits
|
||||||
|
outputCostPer1kTokensInCredits
|
||||||
|
}
|
||||||
billing {
|
billing {
|
||||||
isBillingEnabled
|
isBillingEnabled
|
||||||
billingUrl
|
billingUrl
|
||||||
@ -6426,6 +6521,84 @@ export function useUpdateWorkflowVersionStepMutation(baseOptions?: Apollo.Mutati
|
|||||||
export type UpdateWorkflowVersionStepMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionStepMutation>;
|
export type UpdateWorkflowVersionStepMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionStepMutation>;
|
||||||
export type UpdateWorkflowVersionStepMutationResult = Apollo.MutationResult<UpdateWorkflowVersionStepMutation>;
|
export type UpdateWorkflowVersionStepMutationResult = Apollo.MutationResult<UpdateWorkflowVersionStepMutation>;
|
||||||
export type UpdateWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowVersionStepMutation, UpdateWorkflowVersionStepMutationVariables>;
|
export type UpdateWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowVersionStepMutation, UpdateWorkflowVersionStepMutationVariables>;
|
||||||
|
export const UpdateOneAgentDocument = gql`
|
||||||
|
mutation UpdateOneAgent($input: UpdateAgentInput!) {
|
||||||
|
updateOneAgent(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
prompt
|
||||||
|
modelId
|
||||||
|
responseFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type UpdateOneAgentMutationFn = Apollo.MutationFunction<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpdateOneAgentMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpdateOneAgentMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpdateOneAgentMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [updateOneAgentMutation, { data, loading, error }] = useUpdateOneAgentMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpdateOneAgentMutation(baseOptions?: Apollo.MutationHookOptions<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>(UpdateOneAgentDocument, options);
|
||||||
|
}
|
||||||
|
export type UpdateOneAgentMutationHookResult = ReturnType<typeof useUpdateOneAgentMutation>;
|
||||||
|
export type UpdateOneAgentMutationResult = Apollo.MutationResult<UpdateOneAgentMutation>;
|
||||||
|
export type UpdateOneAgentMutationOptions = Apollo.BaseMutationOptions<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>;
|
||||||
|
export const FindOneAgentDocument = gql`
|
||||||
|
query FindOneAgent($id: UUID!) {
|
||||||
|
findOneAgent(input: {id: $id}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
prompt
|
||||||
|
modelId
|
||||||
|
responseFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useFindOneAgentQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useFindOneAgentQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useFindOneAgentQuery` 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 } = useFindOneAgentQuery({
|
||||||
|
* variables: {
|
||||||
|
* id: // value for 'id'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useFindOneAgentQuery(baseOptions: Apollo.QueryHookOptions<FindOneAgentQuery, FindOneAgentQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<FindOneAgentQuery, FindOneAgentQueryVariables>(FindOneAgentDocument, options);
|
||||||
|
}
|
||||||
|
export function useFindOneAgentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindOneAgentQuery, FindOneAgentQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<FindOneAgentQuery, FindOneAgentQueryVariables>(FindOneAgentDocument, options);
|
||||||
|
}
|
||||||
|
export type FindOneAgentQueryHookResult = ReturnType<typeof useFindOneAgentQuery>;
|
||||||
|
export type FindOneAgentLazyQueryHookResult = ReturnType<typeof useFindOneAgentLazyQuery>;
|
||||||
|
export type FindOneAgentQueryResult = Apollo.QueryResult<FindOneAgentQuery, FindOneAgentQueryVariables>;
|
||||||
export const SubmitFormStepDocument = gql`
|
export const SubmitFormStepDocument = gql`
|
||||||
mutation SubmitFormStep($input: SubmitFormStepInput!) {
|
mutation SubmitFormStep($input: SubmitFormStepInput!) {
|
||||||
submitFormStep(input: $input)
|
submitFormStep(input: $input)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||||
|
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
@ -29,6 +30,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
|
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
|
||||||
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
|
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
|
||||||
const setAuthProviders = useSetRecoilState(authProvidersState);
|
const setAuthProviders = useSetRecoilState(authProvidersState);
|
||||||
|
const setAiModels = useSetRecoilState(aiModelsState);
|
||||||
|
|
||||||
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
|
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
|
||||||
isDeveloperDefaultSignInPrefilledState,
|
isDeveloperDefaultSignInPrefilledState,
|
||||||
@ -134,6 +136,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
magicLink: false,
|
magicLink: false,
|
||||||
sso: data?.clientConfig.authProviders.sso,
|
sso: data?.clientConfig.authProviders.sso,
|
||||||
});
|
});
|
||||||
|
setAiModels(data?.clientConfig.aiModels || []);
|
||||||
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
||||||
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
|
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||||
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
|
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
|
||||||
@ -197,6 +200,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setIsAnalyticsEnabled,
|
setIsAnalyticsEnabled,
|
||||||
setDomainConfiguration,
|
setDomainConfiguration,
|
||||||
setAuthProviders,
|
setAuthProviders,
|
||||||
|
setAiModels,
|
||||||
setCanManageFeatureFlags,
|
setCanManageFeatureFlags,
|
||||||
setLabPublicFeatureFlags,
|
setLabPublicFeatureFlags,
|
||||||
setMicrosoftMessagingEnabled,
|
setMicrosoftMessagingEnabled,
|
||||||
|
|||||||
@ -3,6 +3,13 @@ import { gql } from '@apollo/client';
|
|||||||
export const GET_CLIENT_CONFIG = gql`
|
export const GET_CLIENT_CONFIG = gql`
|
||||||
query GetClientConfig {
|
query GetClientConfig {
|
||||||
clientConfig {
|
clientConfig {
|
||||||
|
aiModels {
|
||||||
|
modelId
|
||||||
|
label
|
||||||
|
provider
|
||||||
|
inputCostPer1kTokensInCredits
|
||||||
|
outputCostPer1kTokensInCredits
|
||||||
|
}
|
||||||
billing {
|
billing {
|
||||||
isBillingEnabled
|
isBillingEnabled
|
||||||
billingUrl
|
billingUrl
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
import { ClientAiModelConfig } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const aiModelsState = createState<ClientAiModelConfig[]>({
|
||||||
|
key: 'aiModelsState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@ -2,10 +2,10 @@ import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
|||||||
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
||||||
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||||
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
|
|
||||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
export const CommandMenuWorkflowSelectActionContent = ({
|
export const CommandMenuWorkflowSelectActionContent = ({
|
||||||
workflow,
|
workflow,
|
||||||
@ -16,6 +16,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
|||||||
const { createStep } = useCreateStep({
|
const { createStep } = useCreateStep({
|
||||||
workflow,
|
workflow,
|
||||||
});
|
});
|
||||||
|
const filteredOtherActions = useFilteredOtherActions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightDrawerStepListContainer>
|
<RightDrawerStepListContainer>
|
||||||
@ -33,7 +34,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
|||||||
<RightDrawerWorkflowSelectStepTitle>
|
<RightDrawerWorkflowSelectStepTitle>
|
||||||
Other
|
Other
|
||||||
</RightDrawerWorkflowSelectStepTitle>
|
</RightDrawerWorkflowSelectStepTitle>
|
||||||
{OTHER_ACTIONS.map((action) => (
|
{filteredOtherActions.map((action) => (
|
||||||
<MenuItemCommand
|
<MenuItemCommand
|
||||||
key={action.type}
|
key={action.type}
|
||||||
LeftIcon={getIcon(action.icon)}
|
LeftIcon={getIcon(action.icon)}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
workflowActionSchema,
|
workflowAiAgentActionSchema,
|
||||||
|
workflowAiAgentActionSettingsSchema,
|
||||||
workflowCodeActionSchema,
|
workflowCodeActionSchema,
|
||||||
workflowCodeActionSettingsSchema,
|
workflowCodeActionSettingsSchema,
|
||||||
workflowCreateRecordActionSchema,
|
workflowCreateRecordActionSchema,
|
||||||
@ -72,7 +73,23 @@ export type WorkflowHttpRequestAction = z.infer<
|
|||||||
typeof workflowHttpRequestActionSchema
|
typeof workflowHttpRequestActionSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type WorkflowAction = z.infer<typeof workflowActionSchema>;
|
export type WorkflowAiAgentActionSettings = z.infer<
|
||||||
|
typeof workflowAiAgentActionSettingsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type WorkflowAiAgentAction = z.infer<typeof workflowAiAgentActionSchema>;
|
||||||
|
|
||||||
|
export type WorkflowAction =
|
||||||
|
| WorkflowCodeAction
|
||||||
|
| WorkflowSendEmailAction
|
||||||
|
| WorkflowCreateRecordAction
|
||||||
|
| WorkflowUpdateRecordAction
|
||||||
|
| WorkflowDeleteRecordAction
|
||||||
|
| WorkflowFindRecordsAction
|
||||||
|
| WorkflowFormAction
|
||||||
|
| WorkflowHttpRequestAction
|
||||||
|
| WorkflowAiAgentAction;
|
||||||
|
|
||||||
export type WorkflowActionType = WorkflowAction['type'];
|
export type WorkflowActionType = WorkflowAction['type'];
|
||||||
export type WorkflowStep = WorkflowAction;
|
export type WorkflowStep = WorkflowAction;
|
||||||
export type WorkflowStepType = WorkflowStep['type'];
|
export type WorkflowStepType = WorkflowStep['type'];
|
||||||
|
|||||||
@ -130,6 +130,13 @@ export const workflowHttpRequestActionSettingsSchema =
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const workflowAiAgentActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
agentId: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
// Action schemas
|
// Action schemas
|
||||||
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||||
type: z.literal('CODE'),
|
type: z.literal('CODE'),
|
||||||
@ -177,6 +184,11 @@ export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({
|
|||||||
settings: workflowHttpRequestActionSettingsSchema,
|
settings: workflowHttpRequestActionSettingsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
|
||||||
|
type: z.literal('AI_AGENT'),
|
||||||
|
settings: workflowAiAgentActionSettingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
// Combined action schema
|
// Combined action schema
|
||||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||||
workflowCodeActionSchema,
|
workflowCodeActionSchema,
|
||||||
@ -187,6 +199,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
|||||||
workflowFindRecordsActionSchema,
|
workflowFindRecordsActionSchema,
|
||||||
workflowFormActionSchema,
|
workflowFormActionSchema,
|
||||||
workflowHttpRequestActionSchema,
|
workflowHttpRequestActionSchema,
|
||||||
|
workflowAiAgentActionSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Trigger schemas
|
// Trigger schemas
|
||||||
|
|||||||
@ -64,6 +64,13 @@ export const WorkflowDiagramStepNodeIcon = ({
|
|||||||
</StyledStepNodeLabelIconContainer>
|
</StyledStepNodeLabelIconContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'AI_AGENT': {
|
||||||
|
return (
|
||||||
|
<StyledStepNodeLabelIconContainer>
|
||||||
|
<Icon size={theme.icon.size.md} color={theme.color.pink} />
|
||||||
|
</StyledStepNodeLabelIconContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
|
|||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
|
||||||
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
||||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||||
@ -188,6 +189,17 @@ export const WorkflowRunStepNodeDetail = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'AI_AGENT': {
|
||||||
|
return (
|
||||||
|
<WorkflowEditActionAiAgent
|
||||||
|
key={stepId}
|
||||||
|
action={stepDefinition.definition}
|
||||||
|
actionOptions={{
|
||||||
|
readonly: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
|
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||||
|
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
|
||||||
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
||||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||||
@ -88,12 +89,12 @@ export const WorkflowStepDetail = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return assertUnreachable(
|
||||||
|
stepDefinition.definition,
|
||||||
|
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return assertUnreachable(
|
|
||||||
stepDefinition.definition,
|
|
||||||
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
case 'action': {
|
case 'action': {
|
||||||
switch (stepDefinition.definition.type) {
|
switch (stepDefinition.definition.type) {
|
||||||
@ -174,12 +175,22 @@ export const WorkflowStepDetail = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'AI_AGENT': {
|
||||||
|
return (
|
||||||
|
<WorkflowEditActionAiAgent
|
||||||
|
key={stepId}
|
||||||
|
action={stepDefinition.definition}
|
||||||
|
actionOptions={props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return assertUnreachable(
|
||||||
|
stepDefinition.definition,
|
||||||
|
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return assertUnreachable(
|
|
||||||
stepDefinition,
|
|
||||||
`Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,118 @@
|
|||||||
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import { WorkflowAiAgentAction } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||||
|
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||||
|
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||||
|
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||||
|
import { BaseOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
|
||||||
|
import { useAgentUpdateFormState } from '../hooks/useAgentUpdateFormState';
|
||||||
|
import { useAiAgentOutputSchema } from '../hooks/useAiAgentOutputSchema';
|
||||||
|
import { useAiModelOptions } from '../hooks/useAiModelOptions';
|
||||||
|
import { WorkflowOutputSchemaBuilder } from './WorkflowOutputSchemaBuilder';
|
||||||
|
|
||||||
|
const StyledErrorMessage = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.danger};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type WorkflowEditActionAiAgentProps = {
|
||||||
|
action: WorkflowAiAgentAction;
|
||||||
|
actionOptions:
|
||||||
|
| { readonly: true }
|
||||||
|
| {
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowAiAgentAction) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowEditActionAiAgent = ({
|
||||||
|
action,
|
||||||
|
actionOptions,
|
||||||
|
}: WorkflowEditActionAiAgentProps) => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||||
|
useWorkflowActionHeader({
|
||||||
|
action,
|
||||||
|
defaultTitle: 'AI Agent',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formValues, handleFieldChange, loading } = useAgentUpdateFormState({
|
||||||
|
agentId: action.settings.input.agentId,
|
||||||
|
readonly: actionOptions.readonly === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOutputSchemaChange, outputFields } = useAiAgentOutputSchema(
|
||||||
|
action.settings.outputSchema as BaseOutputSchema,
|
||||||
|
actionOptions.readonly === true ? undefined : actionOptions.onActionUpdate,
|
||||||
|
action,
|
||||||
|
actionOptions.readonly,
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelOptions = useAiModelOptions();
|
||||||
|
|
||||||
|
const noModelsAvailable = modelOptions.length === 0;
|
||||||
|
|
||||||
|
return loading ? (
|
||||||
|
<RightDrawerSkeletonLoader />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WorkflowStepHeader
|
||||||
|
onTitleChange={(newName: string) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionOptions.onActionUpdate?.({ ...action, name: newName });
|
||||||
|
}}
|
||||||
|
Icon={getIcon(headerIcon)}
|
||||||
|
iconColor={headerIconColor}
|
||||||
|
initialTitle={headerTitle}
|
||||||
|
headerType={headerType}
|
||||||
|
disabled={actionOptions.readonly}
|
||||||
|
/>
|
||||||
|
<WorkflowStepBody>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
dropdownId="select-model"
|
||||||
|
label={t`AI Model`}
|
||||||
|
options={modelOptions}
|
||||||
|
value={formValues.modelId}
|
||||||
|
onChange={(value) => handleFieldChange('modelId', value)}
|
||||||
|
disabled={actionOptions.readonly || noModelsAvailable}
|
||||||
|
emptyOption={{
|
||||||
|
label: t`No AI models available`,
|
||||||
|
value: '',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{noModelsAvailable && (
|
||||||
|
<StyledErrorMessage>
|
||||||
|
{t`You haven't configured any model provider. Please set up an API Key in your instance's admin panel or as an environment variable.`}
|
||||||
|
</StyledErrorMessage>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<FormTextFieldInput
|
||||||
|
key={`prompt-${formValues.modelId ? action.id : 'empty'}`}
|
||||||
|
label={t`Instructions for AI`}
|
||||||
|
placeholder={t`Describe what you want the AI to do...`}
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
defaultValue={formValues.prompt}
|
||||||
|
onChange={(value) => handleFieldChange('prompt', value)}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
<WorkflowOutputSchemaBuilder
|
||||||
|
fields={outputFields}
|
||||||
|
onChange={handleOutputSchemaChange}
|
||||||
|
readonly={actionOptions.readonly}
|
||||||
|
/>
|
||||||
|
</WorkflowStepBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { OUTPUT_FIELD_TYPE_OPTIONS } from '../constants/output-field-type-options';
|
||||||
|
|
||||||
|
type WorkflowOutputFieldTypeSelectorProps = {
|
||||||
|
value?: InputSchemaPropertyType;
|
||||||
|
onChange: (value: InputSchemaPropertyType) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
dropdownId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowOutputFieldTypeSelector = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
dropdownId,
|
||||||
|
}: WorkflowOutputFieldTypeSelectorProps) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
label="Field Type"
|
||||||
|
options={OUTPUT_FIELD_TYPE_OPTIONS.map((option) => ({
|
||||||
|
...option,
|
||||||
|
label: t(option.label),
|
||||||
|
}))}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
|
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||||
|
import { OutputSchemaField } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/output-field-type-options';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { IconPlus, IconTrash } from 'twenty-ui/display';
|
||||||
|
import { LightIconButton } from 'twenty-ui/input';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { WorkflowOutputFieldTypeSelector } from './WorkflowOutputFieldTypeSelector';
|
||||||
|
|
||||||
|
type WorkflowOutputSchemaBuilderProps = {
|
||||||
|
fields: OutputSchemaField[];
|
||||||
|
onChange: (fields: OutputSchemaField[]) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledOutputSchemaContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFieldsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledOutputSchemaFieldContainer = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSettingsContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSettingsHeader = styled.div`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
display: grid;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||||
|
grid-template-columns: 1fr 24px;
|
||||||
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitleContainer = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCloseButtonContainer = styled.div`
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAddFieldButton = styled.button`
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMessageContentContainer = styled.div`
|
||||||
|
flex-direction: column;
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(4)};
|
||||||
|
line-height: normal;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMessageDescription = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowOutputSchemaBuilder = ({
|
||||||
|
fields,
|
||||||
|
onChange,
|
||||||
|
readonly,
|
||||||
|
}: WorkflowOutputSchemaBuilderProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const addField = () => {
|
||||||
|
const newField: OutputSchemaField = {
|
||||||
|
id: v4(),
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
type: 'TEXT' as InputSchemaPropertyType,
|
||||||
|
};
|
||||||
|
onChange([...fields, newField]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeField = (id: string) => {
|
||||||
|
onChange(fields.filter((field) => field.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateField = (id: string, updates: Partial<OutputSchemaField>) => {
|
||||||
|
onChange(
|
||||||
|
fields.map((field) =>
|
||||||
|
field.id === id ? { ...field, ...updates } : field,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledOutputSchemaContainer>
|
||||||
|
<InputLabel>{t`AI Response Schema`}</InputLabel>
|
||||||
|
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<StyledOutputSchemaFieldContainer>
|
||||||
|
<StyledMessageContentContainer>
|
||||||
|
<StyledMessageDescription data-testid="empty-output-schema-message-description">
|
||||||
|
{t`Click on "Add Output Field" below to define the structure of your AI agent's response. These fields will be used to format and validate the AI's output when the workflow is executed, and can be referenced by subsequent workflow steps.`}
|
||||||
|
</StyledMessageDescription>
|
||||||
|
</StyledMessageContentContainer>
|
||||||
|
</StyledOutputSchemaFieldContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<StyledFieldsContainer>
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const fieldNumber = index + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledOutputSchemaFieldContainer key={field.id}>
|
||||||
|
<StyledSettingsHeader>
|
||||||
|
<StyledTitleContainer>
|
||||||
|
<span>{t`Output Field ${fieldNumber}`}</span>
|
||||||
|
</StyledTitleContainer>
|
||||||
|
<StyledCloseButtonContainer>
|
||||||
|
{!readonly && (
|
||||||
|
<LightIconButton
|
||||||
|
testId="close-button"
|
||||||
|
Icon={IconTrash}
|
||||||
|
size="small"
|
||||||
|
accent="secondary"
|
||||||
|
onClick={() => removeField(field.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledCloseButtonContainer>
|
||||||
|
</StyledSettingsHeader>
|
||||||
|
<StyledSettingsContent>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<FormTextFieldInput
|
||||||
|
label={t`Field Name`}
|
||||||
|
placeholder={t`e.g., summary, status, count`}
|
||||||
|
defaultValue={field.name}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateField(field.id, { name: value })
|
||||||
|
}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<WorkflowOutputFieldTypeSelector
|
||||||
|
onChange={(value) =>
|
||||||
|
updateField(field.id, { type: value })
|
||||||
|
}
|
||||||
|
value={field.type}
|
||||||
|
disabled={readonly}
|
||||||
|
dropdownId={`output-field-type-selector-${field.id}`}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<FormTextFieldInput
|
||||||
|
label={t`Description`}
|
||||||
|
placeholder={t`Brief explanation of this output field`}
|
||||||
|
defaultValue={field.description}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateField(field.id, { description: value })
|
||||||
|
}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
</StyledSettingsContent>
|
||||||
|
</StyledOutputSchemaFieldContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledFieldsContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!readonly && (
|
||||||
|
<StyledAddFieldButton onClick={addField}>
|
||||||
|
<IconPlus size={theme.icon.size.sm} />
|
||||||
|
{t`Add Output Field`}
|
||||||
|
</StyledAddFieldButton>
|
||||||
|
)}
|
||||||
|
</StyledOutputSchemaContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import {
|
||||||
|
IllustrationIconCalendarEvent,
|
||||||
|
IllustrationIconNumbers,
|
||||||
|
IllustrationIconText,
|
||||||
|
IllustrationIconToggle,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export interface OutputSchemaField {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: InputSchemaPropertyType | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OUTPUT_FIELD_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
label: msg`Text`,
|
||||||
|
value: FieldMetadataType.TEXT,
|
||||||
|
Icon: IllustrationIconText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg`Number`,
|
||||||
|
value: FieldMetadataType.NUMBER,
|
||||||
|
Icon: IllustrationIconNumbers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg`Boolean`,
|
||||||
|
value: FieldMetadataType.BOOLEAN,
|
||||||
|
Icon: IllustrationIconToggle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: msg`Date`,
|
||||||
|
value: FieldMetadataType.DATE,
|
||||||
|
Icon: IllustrationIconCalendarEvent,
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const UPDATE_ONE_AGENT = gql`
|
||||||
|
mutation UpdateOneAgent($input: UpdateAgentInput!) {
|
||||||
|
updateOneAgent(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
prompt
|
||||||
|
modelId
|
||||||
|
responseFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const FIND_ONE_AGENT = gql`
|
||||||
|
query FindOneAgent($id: UUID!) {
|
||||||
|
findOneAgent(input: { id: $id }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
prompt
|
||||||
|
modelId
|
||||||
|
responseFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { useMutation, useQuery } from '@apollo/client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { UPDATE_ONE_AGENT } from '../graphql/mutations/updateOneAgent';
|
||||||
|
import { FIND_ONE_AGENT } from '../graphql/queries/findOneAgent';
|
||||||
|
|
||||||
|
type AgentFormValues = {
|
||||||
|
name: string;
|
||||||
|
prompt: string;
|
||||||
|
modelId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAgentUpdateFormState = ({
|
||||||
|
agentId,
|
||||||
|
readonly = false,
|
||||||
|
}: {
|
||||||
|
agentId: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [formValues, setFormValues] = useState<AgentFormValues>({
|
||||||
|
name: '',
|
||||||
|
prompt: '',
|
||||||
|
modelId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { loading } = useQuery(FIND_ONE_AGENT, {
|
||||||
|
variables: { id: agentId },
|
||||||
|
skip: !agentId,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (isDefined(data?.findOneAgent)) {
|
||||||
|
const agent = data.findOneAgent;
|
||||||
|
setFormValues({
|
||||||
|
name: agent.name,
|
||||||
|
prompt: agent.prompt,
|
||||||
|
modelId: agent.modelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [updateAgent] = useMutation(UPDATE_ONE_AGENT);
|
||||||
|
|
||||||
|
const updateAgentMutation = async (updates: Partial<AgentFormValues>) => {
|
||||||
|
if (!agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateAgent({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: agentId,
|
||||||
|
...updates,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = useDebouncedCallback(async (formData: AgentFormValues) => {
|
||||||
|
await updateAgentMutation({
|
||||||
|
name: formData.name,
|
||||||
|
prompt: formData.prompt,
|
||||||
|
modelId: formData.modelId,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const handleFieldChange = async (field: string, value: string) => {
|
||||||
|
if (readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormValues((prev) => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
|
await handleSave({ ...formValues, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formValues,
|
||||||
|
handleFieldChange,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { WorkflowAiAgentAction } from '@/workflow/types/Workflow';
|
||||||
|
import { OutputSchemaField } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/output-field-type-options';
|
||||||
|
import { BaseOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { getFieldIcon } from '../utils/getFieldIcon';
|
||||||
|
|
||||||
|
export const useAiAgentOutputSchema = (
|
||||||
|
outputSchema?: BaseOutputSchema,
|
||||||
|
onActionUpdate?: (action: WorkflowAiAgentAction) => void,
|
||||||
|
action?: WorkflowAiAgentAction,
|
||||||
|
readonly?: boolean,
|
||||||
|
) => {
|
||||||
|
const [outputFields, setOutputFields] = useState<OutputSchemaField[]>(
|
||||||
|
Object.entries(outputSchema || {}).map(([name, field]) => ({
|
||||||
|
id: v4(),
|
||||||
|
name,
|
||||||
|
type: field.type,
|
||||||
|
description: field.description,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedSave = useDebouncedCallback(
|
||||||
|
async (fields: OutputSchemaField[]) => {
|
||||||
|
if (readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOutputSchema = fields.reduce<BaseOutputSchema>(
|
||||||
|
(schema, field) => {
|
||||||
|
if (isDefined(field.name)) {
|
||||||
|
schema[field.name] = {
|
||||||
|
isLeaf: true,
|
||||||
|
type: field.type,
|
||||||
|
value: null,
|
||||||
|
icon: getFieldIcon(field.type),
|
||||||
|
label: field.name,
|
||||||
|
description: field.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDefined(onActionUpdate) && isDefined(action)) {
|
||||||
|
onActionUpdate({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
outputSchema: newOutputSchema,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOutputSchemaChange = async (fields: OutputSchemaField[]) => {
|
||||||
|
setOutputFields(fields);
|
||||||
|
await debouncedSave(fields);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleOutputSchemaChange,
|
||||||
|
outputFields,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
export const useAiModelOptions = (): SelectOption<string>[] => {
|
||||||
|
const aiModels = useRecoilValue(aiModelsState);
|
||||||
|
|
||||||
|
return aiModels
|
||||||
|
.map((model) => ({
|
||||||
|
value: model.modelId,
|
||||||
|
label: `${model.label} (${model.provider})`,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export const getFieldIcon = (fieldType?: InputSchemaPropertyType): string => {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldMetadataType.TEXT:
|
||||||
|
return 'IconAbc';
|
||||||
|
case FieldMetadataType.NUMBER:
|
||||||
|
return 'IconText';
|
||||||
|
case FieldMetadataType.BOOLEAN:
|
||||||
|
return 'IconCheckbox';
|
||||||
|
case FieldMetadataType.DATE:
|
||||||
|
return 'IconCalendarEvent';
|
||||||
|
default:
|
||||||
|
return 'IconQuestionMark';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -28,4 +28,9 @@ export const OTHER_ACTIONS: Array<{
|
|||||||
type: 'HTTP_REQUEST',
|
type: 'HTTP_REQUEST',
|
||||||
icon: 'IconWorld',
|
icon: 'IconWorld',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'AI Agent',
|
||||||
|
type: 'AI_AGENT',
|
||||||
|
icon: 'IconBrain',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
import { OTHER_ACTIONS } from '../constants/OtherActions';
|
||||||
|
|
||||||
|
export const useFilteredOtherActions = () => {
|
||||||
|
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||||
|
|
||||||
|
return OTHER_ACTIONS.filter((action) => {
|
||||||
|
return action.type !== 'AI_AGENT' || isAiEnabled;
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -15,6 +15,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
|
|||||||
return msg`Action`;
|
return msg`Action`;
|
||||||
case 'HTTP_REQUEST':
|
case 'HTTP_REQUEST':
|
||||||
return msg`HTTP Request`;
|
return msg`HTTP Request`;
|
||||||
|
case 'AI_AGENT':
|
||||||
|
return msg`AI Agent`;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export const getActionIconColorOrThrow = ({
|
|||||||
return theme.font.color.tertiary;
|
return theme.font.color.tertiary;
|
||||||
case 'SEND_EMAIL':
|
case 'SEND_EMAIL':
|
||||||
return theme.color.blue;
|
return theme.color.blue;
|
||||||
|
case 'AI_AGENT':
|
||||||
|
return theme.color.pink;
|
||||||
default:
|
default:
|
||||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ type Leaf = {
|
|||||||
type?: InputSchemaPropertyType;
|
type?: InputSchemaPropertyType;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
description?: string;
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ type Node = {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
value: OutputSchema;
|
value: OutputSchema;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Link = {
|
type Link = {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
export const mockedClientConfig: ClientConfig = {
|
export const mockedClientConfig: ClientConfig = {
|
||||||
|
aiModels: [],
|
||||||
signInPrefilled: true,
|
signInPrefilled: true,
|
||||||
isMultiWorkspaceEnabled: false,
|
isMultiWorkspaceEnabled: false,
|
||||||
isEmailVerificationRequired: false,
|
isEmailVerificationRequired: false,
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
|
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/anthropic": "^1.2.12",
|
||||||
"@ai-sdk/openai": "^1.3.22",
|
"@ai-sdk/openai": "^1.3.22",
|
||||||
"@blocknote/server-util": "^0.31.1",
|
"@blocknote/server-util": "^0.31.1",
|
||||||
"@clickhouse/client": "^1.11.0",
|
"@clickhouse/client": "^1.11.0",
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateAgentTable1747401483136 implements MigrationInterface {
|
||||||
|
name = 'CreateAgentTable1747401483136';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "core"."agent" (
|
||||||
|
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"name" character varying NOT NULL,
|
||||||
|
"description" character varying,
|
||||||
|
"prompt" text NOT NULL,
|
||||||
|
"modelId" character varying NOT NULL,
|
||||||
|
"responseFormat" jsonb,
|
||||||
|
"workspaceId" uuid NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"deletedAt" TIMESTAMP WITH TIME ZONE,
|
||||||
|
CONSTRAINT "PK_agent" PRIMARY KEY ("id")
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_AGENT_ID_DELETED_AT" ON "core"."agent" ("id", "deletedAt")`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ADD CONSTRAINT "FK_c4cb56621768a4a325dd772bbe1" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" DROP CONSTRAINT "FK_c4cb56621768a4a325dd772bbe1"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "core"."IDX_AGENT_ID_DELETED_AT"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "core"."agent"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,8 @@ import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-f
|
|||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||||
private mainDataSource: DataSource;
|
private mainDataSource: DataSource;
|
||||||
@ -48,6 +50,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
|||||||
WorkspaceSSOIdentityProvider,
|
WorkspaceSSOIdentityProvider,
|
||||||
ApprovedAccessDomain,
|
ApprovedAccessDomain,
|
||||||
TwoFactorMethod,
|
TwoFactorMethod,
|
||||||
|
AgentEntity,
|
||||||
],
|
],
|
||||||
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
||||||
ssl: twentyConfigService.get('PG_SSL_ALLOW_SELF_SIGNED')
|
ssl: twentyConfigService.get('PG_SSL_ALLOW_SELF_SIGNED')
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { AI_DRIVER } from 'src/engine/core-modules/ai/ai.constants';
|
|||||||
import { AiService } from 'src/engine/core-modules/ai/ai.service';
|
import { AiService } from 'src/engine/core-modules/ai/ai.service';
|
||||||
import { AiController } from 'src/engine/core-modules/ai/controllers/ai.controller';
|
import { AiController } from 'src/engine/core-modules/ai/controllers/ai.controller';
|
||||||
import { OpenAIDriver } from 'src/engine/core-modules/ai/drivers/openai.driver';
|
import { OpenAIDriver } from 'src/engine/core-modules/ai/drivers/openai.driver';
|
||||||
|
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@ -33,8 +34,8 @@ export class AiModule {
|
|||||||
module: AiModule,
|
module: AiModule,
|
||||||
imports: [FeatureFlagModule],
|
imports: [FeatureFlagModule],
|
||||||
controllers: [AiController],
|
controllers: [AiController],
|
||||||
providers: [AiService, provider],
|
providers: [AiService, AIBillingService, provider],
|
||||||
exports: [AiService],
|
exports: [AiService, AIBillingService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
export enum ModelProvider {
|
||||||
|
OPENAI = 'openai',
|
||||||
|
ANTHROPIC = 'anthropic',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelId =
|
||||||
|
| 'gpt-4o'
|
||||||
|
| 'gpt-4o-mini'
|
||||||
|
| 'gpt-4-turbo'
|
||||||
|
| 'claude-opus-4-20250514'
|
||||||
|
| 'claude-sonnet-4-20250514'
|
||||||
|
| 'claude-3-5-haiku-20241022';
|
||||||
|
|
||||||
|
export interface AIModelConfig {
|
||||||
|
modelId: ModelId;
|
||||||
|
label: string;
|
||||||
|
provider: ModelProvider;
|
||||||
|
inputCostPer1kTokensInCents: number;
|
||||||
|
outputCostPer1kTokensInCents: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AI_MODELS: AIModelConfig[] = [
|
||||||
|
{
|
||||||
|
modelId: 'gpt-4o',
|
||||||
|
label: 'GPT-4o',
|
||||||
|
provider: ModelProvider.OPENAI,
|
||||||
|
inputCostPer1kTokensInCents: 0.25,
|
||||||
|
outputCostPer1kTokensInCents: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelId: 'gpt-4o-mini',
|
||||||
|
label: 'GPT-4o Mini',
|
||||||
|
provider: ModelProvider.OPENAI,
|
||||||
|
inputCostPer1kTokensInCents: 0.015,
|
||||||
|
outputCostPer1kTokensInCents: 0.06,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelId: 'gpt-4-turbo',
|
||||||
|
label: 'GPT-4 Turbo',
|
||||||
|
provider: ModelProvider.OPENAI,
|
||||||
|
inputCostPer1kTokensInCents: 1.0,
|
||||||
|
outputCostPer1kTokensInCents: 3.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelId: 'claude-opus-4-20250514',
|
||||||
|
label: 'Claude Opus 4',
|
||||||
|
provider: ModelProvider.ANTHROPIC,
|
||||||
|
inputCostPer1kTokensInCents: 1.5,
|
||||||
|
outputCostPer1kTokensInCents: 7.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelId: 'claude-sonnet-4-20250514',
|
||||||
|
label: 'Claude Sonnet 4',
|
||||||
|
provider: ModelProvider.ANTHROPIC,
|
||||||
|
inputCostPer1kTokensInCents: 0.3,
|
||||||
|
outputCostPer1kTokensInCents: 1.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelId: 'claude-3-5-haiku-20241022',
|
||||||
|
label: 'Claude Haiku 3.5',
|
||||||
|
provider: ModelProvider.ANTHROPIC,
|
||||||
|
inputCostPer1kTokensInCents: 0.08,
|
||||||
|
outputCostPer1kTokensInCents: 0.4,
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
// Configuration: $0.001 = 1 credit
|
||||||
|
export const DOLLAR_TO_CREDIT_MULTIPLIER = 1000; // 1 / 0.001 = 1000 credits per dollar
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||||
|
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||||
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
|
|
||||||
|
import { AIBillingService } from './ai-billing.service';
|
||||||
|
|
||||||
|
describe('AIBillingService', () => {
|
||||||
|
let service: AIBillingService;
|
||||||
|
let mockWorkspaceEventEmitter: jest.Mocked<WorkspaceEventEmitter>;
|
||||||
|
|
||||||
|
const mockTokenUsage = {
|
||||||
|
promptTokens: 1000,
|
||||||
|
completionTokens: 500,
|
||||||
|
totalTokens: 1500,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockEventEmitterMethods = {
|
||||||
|
emitCustomBatchEvent: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AIBillingService,
|
||||||
|
{
|
||||||
|
provide: WorkspaceEventEmitter,
|
||||||
|
useValue: mockEventEmitterMethods,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AIBillingService>(AIBillingService);
|
||||||
|
mockWorkspaceEventEmitter = module.get(WorkspaceEventEmitter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calculateCost', () => {
|
||||||
|
it('should calculate cost correctly for valid model and token usage', async () => {
|
||||||
|
const costInCents = await service.calculateCost('gpt-4o', mockTokenUsage);
|
||||||
|
|
||||||
|
// Expected: (1000/1000 * 0.25) + (500/1000 * 1.0) = 0.25 + 0.5 = 0.75 cents
|
||||||
|
expect(costInCents).toBe(0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate cost correctly with different token usage', async () => {
|
||||||
|
const differentTokenUsage = {
|
||||||
|
promptTokens: 2000,
|
||||||
|
completionTokens: 1000,
|
||||||
|
totalTokens: 3000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const costInCents = await service.calculateCost(
|
||||||
|
'gpt-4o',
|
||||||
|
differentTokenUsage,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expected: (2000/1000 * 0.25) + (1000/1000 * 1.0) = 0.5 + 1.0 = 1.5 cents
|
||||||
|
expect(costInCents).toBe(1.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calculateAndBillUsage', () => {
|
||||||
|
it('should calculate cost and emit billing event when model exists', async () => {
|
||||||
|
await service.calculateAndBillUsage(
|
||||||
|
'gpt-4o',
|
||||||
|
mockTokenUsage,
|
||||||
|
'workspace-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expected credits: (0.75 cents / 100) * 1000 = 0.0075 * 1000 = 7.5 credits, rounded to 8
|
||||||
|
expect(
|
||||||
|
mockWorkspaceEventEmitter.emitCustomBatchEvent,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
BILLING_FEATURE_USED,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
eventName: BillingMeterEventName.WORKFLOW_NODE_RUN,
|
||||||
|
value: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'workspace-1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier';
|
||||||
|
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
||||||
|
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||||
|
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||||
|
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||||
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
|
|
||||||
|
export interface TokenUsage {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AIBillingService {
|
||||||
|
private readonly logger = new Logger(AIBillingService.name);
|
||||||
|
|
||||||
|
constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {}
|
||||||
|
|
||||||
|
async calculateCost(modelId: ModelId, usage: TokenUsage): Promise<number> {
|
||||||
|
const model = getAIModelById(modelId);
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
throw new Error(`AI model with id ${modelId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputCost =
|
||||||
|
(usage.promptTokens / 1000) * model.inputCostPer1kTokensInCents;
|
||||||
|
const outputCost =
|
||||||
|
(usage.completionTokens / 1000) * model.outputCostPer1kTokensInCents;
|
||||||
|
|
||||||
|
const totalCost = inputCost + outputCost;
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Calculated cost for model ${modelId}: ${totalCost} cents (input: ${inputCost}, output: ${outputCost})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return totalCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateAndBillUsage(
|
||||||
|
modelId: ModelId,
|
||||||
|
usage: TokenUsage,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const costInCents = await this.calculateCost(modelId, usage);
|
||||||
|
|
||||||
|
const costInDollars = costInCents / 100;
|
||||||
|
const creditsUsed = Math.round(costInDollars * DOLLAR_TO_CREDIT_MULTIPLIER);
|
||||||
|
|
||||||
|
this.sendAiTokenUsageEvent(workspaceId, creditsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendAiTokenUsageEvent(
|
||||||
|
workspaceId: string,
|
||||||
|
creditsUsed: number,
|
||||||
|
): void {
|
||||||
|
this.workspaceEventEmitter.emitCustomBatchEvent<BillingUsageEvent>(
|
||||||
|
BILLING_FEATURE_USED,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
eventName: BillingMeterEventName.WORKFLOW_NODE_RUN,
|
||||||
|
value: creditsUsed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Converts cost in cents to cost in credits
|
||||||
|
* Formula: credits = cents / 100 * 1000 = cents * 10
|
||||||
|
* @param cents - Cost in cents (real cost)
|
||||||
|
* @returns Cost in credits (end-user cost)
|
||||||
|
*/
|
||||||
|
export const convertCentsToCredits = (cents: number): number => cents * 10;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
AIModelConfig,
|
||||||
|
ModelId,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
export const getAIModelById = (modelId: ModelId): AIModelConfig | undefined => {
|
||||||
|
return AI_MODELS.find((model) => model.modelId === modelId);
|
||||||
|
};
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
|
||||||
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
|
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
|
||||||
import { BillingAddWorkflowSubscriptionItemCommand } from 'src/engine/core-modules/billing/commands/billing-add-workflow-subscription-item.command';
|
import { BillingAddWorkflowSubscriptionItemCommand } from 'src/engine/core-modules/billing/commands/billing-add-workflow-subscription-item.command';
|
||||||
import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.command';
|
import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.command';
|
||||||
@ -41,6 +42,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
|||||||
DomainManagerModule,
|
DomainManagerModule,
|
||||||
MessageQueueModule,
|
MessageQueueModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
|
AiModule,
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
[
|
[
|
||||||
BillingSubscription,
|
BillingSubscription,
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
|
|
||||||
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModelId,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
|
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
|
||||||
|
|
||||||
import { ClientConfigController } from './client-config.controller';
|
import { ClientConfigController } from './client-config.controller';
|
||||||
@ -44,6 +48,15 @@ describe('ClientConfigController', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
aiModels: [
|
||||||
|
{
|
||||||
|
modelId: 'gpt-4o' as ModelId,
|
||||||
|
label: 'GPT-4o',
|
||||||
|
provider: ModelProvider.OPENAI,
|
||||||
|
inputCostPer1kTokensInCredits: 2.5,
|
||||||
|
outputCostPer1kTokensInCredits: 10.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
authProviders: {
|
authProviders: {
|
||||||
google: true,
|
google: true,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
@ -92,8 +105,8 @@ describe('ClientConfigController', () => {
|
|||||||
|
|
||||||
const result = await controller.getClientConfig();
|
const result = await controller.getClientConfig();
|
||||||
|
|
||||||
expect(clientConfigService.getClientConfig).toHaveBeenCalled();
|
|
||||||
expect(result).toEqual(mockClientConfig);
|
expect(result).toEqual(mockClientConfig);
|
||||||
|
expect(clientConfigService.getClientConfig).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
|||||||
|
|
||||||
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModelId,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
|
import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
|
||||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
@ -11,6 +15,28 @@ registerEnumType(FeatureFlagKey, {
|
|||||||
name: 'FeatureFlagKey',
|
name: 'FeatureFlagKey',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerEnumType(ModelProvider, {
|
||||||
|
name: 'ModelProvider',
|
||||||
|
});
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ClientAIModelConfig {
|
||||||
|
@Field(() => String)
|
||||||
|
modelId: ModelId;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
@Field(() => ModelProvider)
|
||||||
|
provider: ModelProvider;
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
inputCostPer1kTokensInCredits: number;
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
outputCostPer1kTokensInCredits: number;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class Billing {
|
class Billing {
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@ -88,6 +114,9 @@ export class ClientConfig {
|
|||||||
@Field(() => Billing, { nullable: false })
|
@Field(() => Billing, { nullable: false })
|
||||||
billing: Billing;
|
billing: Billing;
|
||||||
|
|
||||||
|
@Field(() => [ClientAIModelConfig])
|
||||||
|
aiModels: ClientAIModelConfig[];
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
signInPrefilled: boolean;
|
signInPrefilled: boolean;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,10 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
|||||||
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
|
jest.mock('src/engine/core-modules/ai/constants/ai-models.const', () => ({
|
||||||
|
AI_MODELS: [],
|
||||||
|
}));
|
||||||
|
|
||||||
describe('ClientConfigService', () => {
|
describe('ClientConfigService', () => {
|
||||||
let service: ClientConfigService;
|
let service: ClientConfigService;
|
||||||
let twentyConfigService: TwentyConfigService;
|
let twentyConfigService: TwentyConfigService;
|
||||||
@ -107,6 +111,7 @@ describe('ClientConfigService', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
aiModels: [],
|
||||||
authProviders: {
|
authProviders: {
|
||||||
google: true,
|
google: true,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
@ -164,6 +169,7 @@ describe('ClientConfigService', () => {
|
|||||||
|
|
||||||
expect(result.debugMode).toBe(false);
|
expect(result.debugMode).toBe(false);
|
||||||
expect(result.canManageFeatureFlags).toBe(false);
|
expect(result.canManageFeatureFlags).toBe(false);
|
||||||
|
expect(result.aiModels).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing captcha driver', async () => {
|
it('should handle missing captcha driver', async () => {
|
||||||
@ -180,6 +186,7 @@ describe('ClientConfigService', () => {
|
|||||||
|
|
||||||
expect(result.captcha.provider).toBeUndefined();
|
expect(result.captcha.provider).toBeUndefined();
|
||||||
expect(result.captcha.siteKey).toBe('site-key');
|
expect(result.captcha.siteKey).toBe('site-key');
|
||||||
|
expect(result.aiModels).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing support driver', async () => {
|
it('should handle missing support driver', async () => {
|
||||||
@ -194,6 +201,7 @@ describe('ClientConfigService', () => {
|
|||||||
const result = await service.getClientConfig();
|
const result = await service.getClientConfig();
|
||||||
|
|
||||||
expect(result.support.supportDriver).toBe(SupportDriver.NONE);
|
expect(result.support.supportDriver).toBe(SupportDriver.NONE);
|
||||||
|
expect(result.aiModels).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle billing enabled with feature flags', async () => {
|
it('should handle billing enabled with feature flags', async () => {
|
||||||
|
|||||||
@ -3,7 +3,15 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||||
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
||||||
|
|
||||||
import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity';
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
import { convertCentsToCredits } from 'src/engine/core-modules/ai/utils/ai-cost.utils';
|
||||||
|
import {
|
||||||
|
ClientAIModelConfig,
|
||||||
|
ClientConfig,
|
||||||
|
} from 'src/engine/core-modules/client-config/client-config.entity';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
@ -18,6 +26,32 @@ export class ClientConfigService {
|
|||||||
async getClientConfig(): Promise<ClientConfig> {
|
async getClientConfig(): Promise<ClientConfig> {
|
||||||
const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER');
|
const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER');
|
||||||
const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER');
|
const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER');
|
||||||
|
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||||
|
const anthropicApiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
||||||
|
|
||||||
|
const aiModels = AI_MODELS.reduce<ClientAIModelConfig[]>((acc, model) => {
|
||||||
|
const isAvailable =
|
||||||
|
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
|
||||||
|
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
|
||||||
|
|
||||||
|
if (!isAvailable) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
modelId: model.modelId,
|
||||||
|
label: model.label,
|
||||||
|
provider: model.provider,
|
||||||
|
inputCostPer1kTokensInCredits: convertCentsToCredits(
|
||||||
|
model.inputCostPer1kTokensInCents,
|
||||||
|
),
|
||||||
|
outputCostPer1kTokensInCredits: convertCentsToCredits(
|
||||||
|
model.outputCostPer1kTokensInCents,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clientConfig: ClientConfig = {
|
const clientConfig: ClientConfig = {
|
||||||
billing: {
|
billing: {
|
||||||
@ -38,6 +72,7 @@ export class ClientConfigService {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
aiModels,
|
||||||
authProviders: {
|
authProviders: {
|
||||||
google: this.twentyConfigService.get('AUTH_GOOGLE_ENABLED'),
|
google: this.twentyConfigService.get('AUTH_GOOGLE_ENABLED'),
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
|
|||||||
@ -984,6 +984,14 @@ export class ConfigVariables {
|
|||||||
})
|
})
|
||||||
OPENAI_API_KEY: string;
|
OPENAI_API_KEY: string;
|
||||||
|
|
||||||
|
@ConfigVariablesMetadata({
|
||||||
|
group: ConfigVariablesGroup.LLM,
|
||||||
|
isSensitive: true,
|
||||||
|
description: 'API key for Anthropic integration',
|
||||||
|
type: ConfigVariableType.STRING,
|
||||||
|
})
|
||||||
|
ANTHROPIC_API_KEY: string;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.ServerConfig,
|
group: ConfigVariablesGroup.ServerConfig,
|
||||||
description: 'Enable or disable multi-workspace support',
|
description: 'Enable or disable multi-workspace support',
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
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 { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||||
import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto';
|
import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto';
|
||||||
import { SubmitFormStepInput } from 'src/engine/core-modules/workflow/dtos/submit-form-step-input.dto';
|
import { SubmitFormStepInput } from 'src/engine/core-modules/workflow/dtos/submit-form-step-input.dto';
|
||||||
@ -15,6 +17,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
|||||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||||
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
|
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
|
||||||
|
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -28,6 +31,7 @@ export class WorkflowStepResolver {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService,
|
private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService,
|
||||||
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Mutation(() => WorkflowActionDTO)
|
@Mutation(() => WorkflowActionDTO)
|
||||||
@ -36,6 +40,19 @@ export class WorkflowStepResolver {
|
|||||||
@Args('input')
|
@Args('input')
|
||||||
input: CreateWorkflowVersionStepInput,
|
input: CreateWorkflowVersionStepInput,
|
||||||
): Promise<WorkflowActionDTO> {
|
): Promise<WorkflowActionDTO> {
|
||||||
|
if (input.stepType === WorkflowActionType.AI_AGENT) {
|
||||||
|
const isAiEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_AI_ENABLED,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAiEnabled) {
|
||||||
|
throw new Error(
|
||||||
|
'AI features are not available in your current workspace. Please contact support to enable them.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this.workflowVersionStepWorkspaceService.createWorkflowVersionStep({
|
return this.workflowVersionStepWorkspaceService.createWorkflowVersionStep({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller';
|
import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller';
|
||||||
import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver';
|
import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver';
|
||||||
import { WorkflowStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-step.resolver';
|
import { WorkflowStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-step.resolver';
|
||||||
@ -14,6 +15,7 @@ import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/wor
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
FeatureFlagModule,
|
||||||
WorkflowTriggerModule,
|
WorkflowTriggerModule,
|
||||||
WorkflowBuilderModule,
|
WorkflowBuilderModule,
|
||||||
WorkflowCommonModule,
|
WorkflowCommonModule,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
|
|||||||
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||||
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
|
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||||
|
|
||||||
registerEnumType(WorkspaceActivationStatus, {
|
registerEnumType(WorkspaceActivationStatus, {
|
||||||
@ -121,6 +122,11 @@ export class Workspace {
|
|||||||
)
|
)
|
||||||
workspaceSSOIdentityProviders: Relation<WorkspaceSSOIdentityProvider[]>;
|
workspaceSSOIdentityProviders: Relation<WorkspaceSSOIdentityProvider[]>;
|
||||||
|
|
||||||
|
@OneToMany(() => AgentEntity, (agent) => agent.workspace, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
agents: Relation<AgentEntity[]>;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
@Column({ default: 1 })
|
@Column({ default: 1 })
|
||||||
metadataVersion: number;
|
metadataVersion: number;
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
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 { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
|
export const FEATURE_FLAG_KEY = 'feature-flag-metadata-args';
|
||||||
|
|
||||||
|
export function RequireFeatureFlag(featureFlag: FeatureFlagKey) {
|
||||||
|
return (
|
||||||
|
target: object,
|
||||||
|
propertyKey?: string,
|
||||||
|
descriptor?: PropertyDescriptor,
|
||||||
|
) => {
|
||||||
|
TypedReflect.defineMetadata(
|
||||||
|
FEATURE_FLAG_KEY,
|
||||||
|
featureFlag,
|
||||||
|
descriptor?.value || target,
|
||||||
|
);
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FeatureFlagGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const ctx = GqlExecutionContext.create(context);
|
||||||
|
const request = ctx.getContext().req;
|
||||||
|
const workspaceId = request.workspace?.id;
|
||||||
|
|
||||||
|
if (!workspaceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureFlag = this.reflector.get<FeatureFlagKey>(
|
||||||
|
FEATURE_FLAG_KEY,
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!featureFlag) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||||
|
featureFlag,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
throw new Error(
|
||||||
|
`Feature flag "${featureFlag}" is not enabled for this workspace`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||||
|
import { createOpenAI } from '@ai-sdk/openai';
|
||||||
|
import { generateObject } from 'ai';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModelId,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||||
|
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||||
|
|
||||||
|
import { AgentEntity } from './agent.entity';
|
||||||
|
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||||
|
|
||||||
|
import { convertOutputSchemaToZod } from './utils/convert-output-schema-to-zod';
|
||||||
|
|
||||||
|
export interface AgentExecutionResult {
|
||||||
|
object: object;
|
||||||
|
usage: {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentExecutionService {
|
||||||
|
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||||
|
|
||||||
|
private getModel = (modelId: ModelId, provider: ModelProvider) => {
|
||||||
|
switch (provider) {
|
||||||
|
case ModelProvider.OPENAI: {
|
||||||
|
const OpenAIProvider = createOpenAI({
|
||||||
|
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return OpenAIProvider(modelId);
|
||||||
|
}
|
||||||
|
case ModelProvider.ANTHROPIC: {
|
||||||
|
const AnthropicProvider = createAnthropic({
|
||||||
|
apiKey: this.twentyConfigService.get('ANTHROPIC_API_KEY'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return AnthropicProvider(modelId);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new AgentException(
|
||||||
|
`Unsupported provider: ${provider}`,
|
||||||
|
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private async validateApiKey(provider: ModelProvider): Promise<void> {
|
||||||
|
let apiKey: string | undefined;
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case ModelProvider.OPENAI:
|
||||||
|
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||||
|
break;
|
||||||
|
case ModelProvider.ANTHROPIC:
|
||||||
|
apiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new AgentException(
|
||||||
|
`Unsupported provider: ${provider}`,
|
||||||
|
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new AgentException(
|
||||||
|
`${provider.toUpperCase()} API key not configured`,
|
||||||
|
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeAgent({
|
||||||
|
agent,
|
||||||
|
context,
|
||||||
|
schema,
|
||||||
|
}: {
|
||||||
|
agent: AgentEntity;
|
||||||
|
context: Record<string, unknown>;
|
||||||
|
schema: OutputSchema;
|
||||||
|
}): Promise<AgentExecutionResult> {
|
||||||
|
try {
|
||||||
|
const aiModel = getAIModelById(agent.modelId);
|
||||||
|
|
||||||
|
if (!aiModel) {
|
||||||
|
throw new AgentException(
|
||||||
|
`AI model with id ${agent.modelId} not found`,
|
||||||
|
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = aiModel.provider;
|
||||||
|
|
||||||
|
await this.validateApiKey(provider);
|
||||||
|
|
||||||
|
const output = await generateObject({
|
||||||
|
model: this.getModel(agent.modelId, provider),
|
||||||
|
prompt: resolveInput(agent.prompt, context) as string,
|
||||||
|
schema: convertOutputSchemaToZod(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
object: output.object,
|
||||||
|
usage: {
|
||||||
|
promptTokens: output.usage?.promptTokens ?? 0,
|
||||||
|
completionTokens: output.usage?.completionTokens ?? 0,
|
||||||
|
totalTokens: output.usage?.totalTokens,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AgentException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AgentException(
|
||||||
|
error instanceof Error ? error.message : 'Agent execution failed',
|
||||||
|
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
|
@Entity('agent')
|
||||||
|
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||||
|
export class AgentEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false, type: 'text' })
|
||||||
|
prompt: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false, type: 'varchar' })
|
||||||
|
modelId: ModelId;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'jsonb' })
|
||||||
|
responseFormat: object;
|
||||||
|
|
||||||
|
@Column({ nullable: false, type: 'uuid' })
|
||||||
|
workspaceId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Workspace, (workspace) => workspace.agents, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'workspaceId' })
|
||||||
|
workspace: Relation<Workspace>;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
|
deletedAt?: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
|
export class AgentException extends CustomException {
|
||||||
|
declare code: AgentExceptionCode;
|
||||||
|
constructor(message: string, code: AgentExceptionCode) {
|
||||||
|
super(message, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AgentExceptionCode {
|
||||||
|
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
||||||
|
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
||||||
|
AGENT_ALREADY_EXISTS = 'AGENT_ALREADY_EXISTS',
|
||||||
|
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
||||||
|
AGENT_EXECUTION_LIMIT_REACHED = 'AGENT_EXECUTION_LIMIT_REACHED',
|
||||||
|
AGENT_INVALID_PROMPT = 'AGENT_INVALID_PROMPT',
|
||||||
|
AGENT_INVALID_MODEL = 'AGENT_INVALID_MODEL',
|
||||||
|
UNSUPPORTED_MODEL = 'UNSUPPORTED_MODEL',
|
||||||
|
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
|
||||||
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||||
|
|
||||||
|
import { AgentExecutionService } from './agent-execution.service';
|
||||||
|
import { AgentEntity } from './agent.entity';
|
||||||
|
import { AgentResolver } from './agent.resolver';
|
||||||
|
import { AgentService } from './agent.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([AgentEntity, FeatureFlag], 'core'),
|
||||||
|
AiModule,
|
||||||
|
ThrottlerModule,
|
||||||
|
AuditModule,
|
||||||
|
FeatureFlagModule,
|
||||||
|
],
|
||||||
|
providers: [AgentResolver, AgentService, AgentExecutionService],
|
||||||
|
exports: [
|
||||||
|
AgentService,
|
||||||
|
AgentExecutionService,
|
||||||
|
TypeOrmModule.forFeature([AgentEntity], 'core'),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AgentModule {}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
|
import {
|
||||||
|
FeatureFlagGuard,
|
||||||
|
RequireFeatureFlag,
|
||||||
|
} from 'src/engine/guards/feature-flag.guard';
|
||||||
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
|
||||||
|
import { AgentService } from './agent.service';
|
||||||
|
|
||||||
|
import { AgentIdInput } from './dtos/agent-id.input';
|
||||||
|
import { AgentDTO } from './dtos/agent.dto';
|
||||||
|
import { CreateAgentInput } from './dtos/create-agent.input';
|
||||||
|
import { UpdateAgentInput } from './dtos/update-agent.input';
|
||||||
|
|
||||||
|
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||||
|
@Resolver()
|
||||||
|
export class AgentResolver {
|
||||||
|
constructor(private readonly agentService: AgentService) {}
|
||||||
|
|
||||||
|
@Query(() => AgentDTO)
|
||||||
|
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||||
|
async findOneAgent(
|
||||||
|
@Args('input') { id }: AgentIdInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.agentService.findOneAgent(id, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => [AgentDTO])
|
||||||
|
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||||
|
async findManyAgents(@AuthWorkspace() { id: workspaceId }: Workspace) {
|
||||||
|
return this.agentService.findManyAgents(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AgentDTO)
|
||||||
|
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||||
|
async createOneAgent(
|
||||||
|
@Args('input') input: CreateAgentInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.agentService.createOneAgent(input, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AgentDTO)
|
||||||
|
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||||
|
async updateOneAgent(
|
||||||
|
@Args('input') input: UpdateAgentInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.agentService.updateOneAgent(input, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AgentDTO)
|
||||||
|
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||||
|
async deleteOneAgent(
|
||||||
|
@Args('input') { id }: AgentIdInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.agentService.deleteOneAgent(id, workspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { AgentEntity } from './agent.entity';
|
||||||
|
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AgentEntity, 'core')
|
||||||
|
private readonly agentRepository: Repository<AgentEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findManyAgents(workspaceId: string) {
|
||||||
|
return this.agentRepository.find({
|
||||||
|
where: { workspaceId },
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneAgent(id: string, workspaceId: string) {
|
||||||
|
const agent = await this.agentRepository.findOne({
|
||||||
|
where: { id, workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
throw new AgentException(
|
||||||
|
`Agent with id ${id} not found`,
|
||||||
|
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOneAgent(
|
||||||
|
input: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
prompt: string;
|
||||||
|
modelId: ModelId;
|
||||||
|
responseFormat?: object;
|
||||||
|
},
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const agent = this.agentRepository.create({
|
||||||
|
...input,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdAgent = await this.agentRepository.save(agent);
|
||||||
|
|
||||||
|
return this.findOneAgent(createdAgent.id, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOneAgent(
|
||||||
|
input: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
prompt?: string;
|
||||||
|
modelId?: ModelId;
|
||||||
|
responseFormat?: object;
|
||||||
|
},
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const agent = await this.findOneAgent(input.id, workspaceId);
|
||||||
|
|
||||||
|
const updatedAgent = await this.agentRepository.save({
|
||||||
|
...agent,
|
||||||
|
...input,
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOneAgent(id: string, workspaceId: string) {
|
||||||
|
const agent = await this.findOneAgent(id, workspaceId);
|
||||||
|
|
||||||
|
await this.agentRepository.softDelete({ id: agent.id });
|
||||||
|
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class AgentIdInput {
|
||||||
|
@Field(() => UUIDScalarType, { description: 'The id of the agent.' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsDateString, IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
@ObjectType('Agent')
|
||||||
|
export class AgentDTO {
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field(() => UUIDScalarType)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Field()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
prompt: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
modelId: ModelId;
|
||||||
|
|
||||||
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
|
responseFormat: object;
|
||||||
|
|
||||||
|
@HideField()
|
||||||
|
workspaceId: string;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
@Field()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
@Field()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class CreateAgentInput {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
prompt: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field(() => String)
|
||||||
|
modelId: ModelId;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
|
responseFormat?: object;
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IsNotEmpty,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
} from 'class-validator';
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class UpdateAgentInput {
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field(() => UUIDScalarType)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Field()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Field()
|
||||||
|
prompt?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Field(() => String)
|
||||||
|
modelId?: ModelId;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
|
responseFormat?: object;
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||||
|
|
||||||
|
export const convertOutputSchemaToZod = (
|
||||||
|
schema: OutputSchema,
|
||||||
|
): z.ZodObject<Record<string, z.ZodTypeAny>> => {
|
||||||
|
const shape: Record<string, z.ZodTypeAny> = {};
|
||||||
|
|
||||||
|
for (const [fieldName, field] of Object.entries(schema)) {
|
||||||
|
if (field.isLeaf) {
|
||||||
|
let fieldSchema: z.ZodTypeAny;
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case 'TEXT':
|
||||||
|
fieldSchema = z.string();
|
||||||
|
break;
|
||||||
|
case 'NUMBER':
|
||||||
|
fieldSchema = z.number();
|
||||||
|
break;
|
||||||
|
case 'BOOLEAN':
|
||||||
|
fieldSchema = z.boolean();
|
||||||
|
break;
|
||||||
|
case 'DATE':
|
||||||
|
fieldSchema = z.string().describe('Date-time string');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported field type for AI agent output: ${field.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.description) {
|
||||||
|
fieldSchema = fieldSchema.describe(field.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
shape[fieldName] = fieldSchema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return z.object(shape);
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
@ -16,6 +17,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
|||||||
FieldMetadataModule,
|
FieldMetadataModule,
|
||||||
ObjectMetadataModule,
|
ObjectMetadataModule,
|
||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
|
AgentModule,
|
||||||
WorkspaceMetadataVersionModule,
|
WorkspaceMetadataVersionModule,
|
||||||
WorkspaceMigrationModule,
|
WorkspaceMigrationModule,
|
||||||
RemoteServerModule,
|
RemoteServerModule,
|
||||||
@ -28,6 +30,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
|||||||
FieldMetadataModule,
|
FieldMetadataModule,
|
||||||
ObjectMetadataModule,
|
ObjectMetadataModule,
|
||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
|
AgentModule,
|
||||||
RemoteServerModule,
|
RemoteServerModule,
|
||||||
RoleModule,
|
RoleModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
|||||||
|
|
||||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||||
@ -20,6 +21,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
|
|||||||
FileModule,
|
FileModule,
|
||||||
ThrottlerModule,
|
ThrottlerModule,
|
||||||
AuditModule,
|
AuditModule,
|
||||||
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
||||||
exports: [ServerlessFunctionService],
|
exports: [ServerlessFunctionService],
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import graphqlTypeJson from 'graphql-type-json';
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
|
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||||
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
||||||
@ -21,13 +21,11 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
|
|||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ServerlessFunctionResolver {
|
export class ServerlessFunctionResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
@InjectRepository(FeatureFlag, 'core')
|
|
||||||
private readonly featureFlagRepository: Repository<FeatureFlag>,
|
|
||||||
@InjectRepository(ServerlessFunctionEntity, 'core')
|
@InjectRepository(ServerlessFunctionEntity, 'core')
|
||||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export type Leaf = {
|
|||||||
type?: InputSchemaPropertyType;
|
type?: InputSchemaPropertyType;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
description?: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
@ -14,6 +15,7 @@ export type Node = {
|
|||||||
type?: InputSchemaPropertyType;
|
type?: InputSchemaPropertyType;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
description?: string;
|
||||||
value: OutputSchema;
|
value: OutputSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||||
|
|
||||||
|
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
||||||
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
|
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
|
||||||
@ -11,6 +12,7 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
AgentModule,
|
||||||
WorkflowSchemaModule,
|
WorkflowSchemaModule,
|
||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
WorkflowRunnerModule,
|
WorkflowRunnerModule,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { v4 } from 'uuid';
|
|||||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||||
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||||
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
@ -50,6 +51,7 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,
|
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
|
private readonly agentService: AgentService,
|
||||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
||||||
@ -350,11 +352,13 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
}): Promise<WorkflowAction> {
|
}): Promise<WorkflowAction> {
|
||||||
// We don't enrich on the fly for code and HTTP request workflow actions.
|
// We don't enrich on the fly for code and HTTP request workflow actions.
|
||||||
// For code actions, OutputSchema is computed and updated when testing the serverless function.
|
// For code actions, OutputSchema is computed and updated when testing the serverless function.
|
||||||
// For HTTP requests, OutputSchema is determined by the expamle response input
|
// For HTTP requests and AI agent, OutputSchema is determined by the expamle response input
|
||||||
if (
|
if (
|
||||||
[WorkflowActionType.CODE, WorkflowActionType.HTTP_REQUEST].includes(
|
[
|
||||||
step.type,
|
WorkflowActionType.CODE,
|
||||||
)
|
WorkflowActionType.HTTP_REQUEST,
|
||||||
|
WorkflowActionType.AI_AGENT,
|
||||||
|
].includes(step.type)
|
||||||
) {
|
) {
|
||||||
return step;
|
return step;
|
||||||
}
|
}
|
||||||
@ -396,6 +400,17 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case WorkflowActionType.AI_AGENT: {
|
||||||
|
const agent = await this.agentService.findOneAgent(
|
||||||
|
step.settings.input.agentId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (agent) {
|
||||||
|
await this.agentService.deleteOneAgent(agent.id, workspaceId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,6 +593,37 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case WorkflowActionType.AI_AGENT: {
|
||||||
|
const newAgent = await this.agentService.createOneAgent(
|
||||||
|
{
|
||||||
|
name: 'AI Agent Workflow Step',
|
||||||
|
description: 'Created automatically for workflow step',
|
||||||
|
prompt: '',
|
||||||
|
modelId: 'gpt-4o',
|
||||||
|
},
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(newAgent)) {
|
||||||
|
throw new WorkflowVersionStepException(
|
||||||
|
'Failed to create AI Agent Step',
|
||||||
|
WorkflowVersionStepExceptionCode.FAILURE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newStepId,
|
||||||
|
name: 'AI Agent',
|
||||||
|
type: WorkflowActionType.AI_AGENT,
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
...BASE_STEP_DEFINITION,
|
||||||
|
input: {
|
||||||
|
agentId: newAgent.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new WorkflowVersionStepException(
|
throw new WorkflowVersionStepException(
|
||||||
`WorkflowActionType '${type}' unknown`,
|
`WorkflowActionType '${type}' unknown`,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
WorkflowStepExecutorException,
|
WorkflowStepExecutorException,
|
||||||
WorkflowStepExecutorExceptionCode,
|
WorkflowStepExecutorExceptionCode,
|
||||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||||
|
import { AiAgentWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action';
|
||||||
import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action';
|
import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action';
|
||||||
import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action';
|
import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action';
|
||||||
import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action';
|
import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action';
|
||||||
@ -29,6 +30,7 @@ export class WorkflowExecutorFactory {
|
|||||||
private readonly formWorkflowAction: FormWorkflowAction,
|
private readonly formWorkflowAction: FormWorkflowAction,
|
||||||
private readonly filterWorkflowAction: FilterWorkflowAction,
|
private readonly filterWorkflowAction: FilterWorkflowAction,
|
||||||
private readonly httpRequestWorkflowAction: HttpRequestWorkflowAction,
|
private readonly httpRequestWorkflowAction: HttpRequestWorkflowAction,
|
||||||
|
private readonly aiAgentWorkflowAction: AiAgentWorkflowAction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get(stepType: WorkflowActionType): WorkflowExecutor {
|
get(stepType: WorkflowActionType): WorkflowExecutor {
|
||||||
@ -51,6 +53,8 @@ export class WorkflowExecutorFactory {
|
|||||||
return this.filterWorkflowAction;
|
return this.filterWorkflowAction;
|
||||||
case WorkflowActionType.HTTP_REQUEST:
|
case WorkflowActionType.HTTP_REQUEST:
|
||||||
return this.httpRequestWorkflowAction;
|
return this.httpRequestWorkflowAction;
|
||||||
|
case WorkflowActionType.AI_AGENT:
|
||||||
|
return this.aiAgentWorkflowAction;
|
||||||
default:
|
default:
|
||||||
throw new WorkflowStepExecutorException(
|
throw new WorkflowStepExecutorException(
|
||||||
`Workflow step executor not found for step type '${stepType}'`,
|
`Workflow step executor not found for step type '${stepType}'`,
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { AiDriver } from 'src/engine/core-modules/ai/interfaces/ai.interface';
|
||||||
|
|
||||||
|
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
|
||||||
|
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||||
|
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||||
|
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||||
|
|
||||||
|
import { AiAgentWorkflowAction } from './ai-agent.workflow-action';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
AgentModule,
|
||||||
|
AiModule.forRoot({
|
||||||
|
useFactory: () => ({ type: AiDriver.OPENAI }),
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forFeature([AgentEntity], 'core'),
|
||||||
|
],
|
||||||
|
providers: [ScopedWorkspaceContextFactory, AiAgentWorkflowAction],
|
||||||
|
exports: [AiAgentWorkflowAction],
|
||||||
|
})
|
||||||
|
export class AiAgentActionModule {}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
|
||||||
|
|
||||||
|
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||||
|
import { AgentExecutionService } from 'src/engine/metadata-modules/agent/agent-execution.service';
|
||||||
|
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||||
|
import {
|
||||||
|
AgentException,
|
||||||
|
AgentExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||||
|
import {
|
||||||
|
WorkflowStepExecutorException,
|
||||||
|
WorkflowStepExecutorExceptionCode,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||||
|
import { WorkflowExecutorInput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||||
|
import { WorkflowExecutorOutput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-output.type';
|
||||||
|
|
||||||
|
import { isWorkflowAiAgentAction } from './guards/is-workflow-ai-agent-action.guard';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AiAgentWorkflowAction implements WorkflowExecutor {
|
||||||
|
constructor(
|
||||||
|
private readonly agentExecutionService: AgentExecutionService,
|
||||||
|
private readonly aiBillingService: AIBillingService,
|
||||||
|
@InjectRepository(AgentEntity, 'core')
|
||||||
|
private readonly agentRepository: Repository<AgentEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute({
|
||||||
|
currentStepId,
|
||||||
|
steps,
|
||||||
|
context,
|
||||||
|
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
|
||||||
|
const step = steps.find((step) => step.id === currentStepId);
|
||||||
|
|
||||||
|
if (!step) {
|
||||||
|
throw new WorkflowStepExecutorException(
|
||||||
|
'Step not found',
|
||||||
|
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWorkflowAiAgentAction(step)) {
|
||||||
|
throw new WorkflowStepExecutorException(
|
||||||
|
'Step is not an AI Agent action',
|
||||||
|
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { agentId } = step.settings.input;
|
||||||
|
const workspaceId = context.workspaceId as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = await this.agentRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: agentId,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
throw new AgentException(
|
||||||
|
`Agent with id ${agentId} not found`,
|
||||||
|
AgentExceptionCode.AGENT_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionResult = await this.agentExecutionService.executeAgent({
|
||||||
|
agent,
|
||||||
|
context,
|
||||||
|
schema: step.settings.outputSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.aiBillingService.calculateAndBillUsage(
|
||||||
|
agent.modelId,
|
||||||
|
executionResult.usage,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { result: executionResult.object };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AgentException) {
|
||||||
|
return {
|
||||||
|
error: `${error.message} (${error.code})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
error instanceof Error ? error.message : 'AI Agent execution failed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
WorkflowAction,
|
||||||
|
WorkflowActionType,
|
||||||
|
WorkflowAiAgentAction,
|
||||||
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
|
|
||||||
|
export const isWorkflowAiAgentAction = (
|
||||||
|
action: WorkflowAction,
|
||||||
|
): action is WorkflowAiAgentAction => {
|
||||||
|
return action.type === WorkflowActionType.AI_AGENT;
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export type WorkflowAiAgentActionInput = {
|
||||||
|
agentId: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { WorkflowAiAgentActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/types/workflow-ai-agent-action-input.type';
|
||||||
|
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||||
|
|
||||||
|
export type WorkflowAiAgentActionSettings = BaseWorkflowActionSettings & {
|
||||||
|
input: WorkflowAiAgentActionInput;
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||||
|
import { WorkflowAiAgentActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/types/workflow-ai-agent-action-settings.type';
|
||||||
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
|
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
|
||||||
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||||
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||||
@ -32,4 +33,5 @@ export type WorkflowActionSettings =
|
|||||||
| WorkflowFindRecordsActionSettings
|
| WorkflowFindRecordsActionSettings
|
||||||
| WorkflowFormActionSettings
|
| WorkflowFormActionSettings
|
||||||
| WorkflowFilterActionSettings
|
| WorkflowFilterActionSettings
|
||||||
| WorkflowHttpRequestActionSettings;
|
| WorkflowHttpRequestActionSettings
|
||||||
|
| WorkflowAiAgentActionSettings;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { WorkflowAiAgentActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/types/workflow-ai-agent-action-settings.type';
|
||||||
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
|
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
|
||||||
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||||
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||||
@ -21,6 +22,7 @@ export enum WorkflowActionType {
|
|||||||
FORM = 'FORM',
|
FORM = 'FORM',
|
||||||
FILTER = 'FILTER',
|
FILTER = 'FILTER',
|
||||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||||
|
AI_AGENT = 'AI_AGENT',
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseWorkflowAction = {
|
type BaseWorkflowAction = {
|
||||||
@ -77,6 +79,11 @@ export type WorkflowHttpRequestAction = BaseWorkflowAction & {
|
|||||||
settings: WorkflowHttpRequestActionSettings;
|
settings: WorkflowHttpRequestActionSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowAiAgentAction = BaseWorkflowAction & {
|
||||||
|
type: WorkflowActionType.AI_AGENT;
|
||||||
|
settings: WorkflowAiAgentActionSettings;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowAction =
|
export type WorkflowAction =
|
||||||
| WorkflowCodeAction
|
| WorkflowCodeAction
|
||||||
| WorkflowSendEmailAction
|
| WorkflowSendEmailAction
|
||||||
@ -86,4 +93,5 @@ export type WorkflowAction =
|
|||||||
| WorkflowFindRecordsAction
|
| WorkflowFindRecordsAction
|
||||||
| WorkflowFormAction
|
| WorkflowFormAction
|
||||||
| WorkflowFilterAction
|
| WorkflowFilterAction
|
||||||
| WorkflowHttpRequestAction;
|
| WorkflowHttpRequestAction
|
||||||
|
| WorkflowAiAgentAction;
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||||
import { WorkflowExecutorFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-executor.factory';
|
import { WorkflowExecutorFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-executor.factory';
|
||||||
|
import { AiAgentActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent-action.module';
|
||||||
import { CodeActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code-action.module';
|
import { CodeActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code-action.module';
|
||||||
import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module';
|
import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module';
|
||||||
import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module';
|
import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module';
|
||||||
@ -24,6 +26,8 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
|
|||||||
BillingModule,
|
BillingModule,
|
||||||
FilterActionModule,
|
FilterActionModule,
|
||||||
HttpRequestActionModule,
|
HttpRequestActionModule,
|
||||||
|
AiAgentActionModule,
|
||||||
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
WorkflowExecutorWorkspaceService,
|
WorkflowExecutorWorkspaceService,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'reflect-metadata';
|
|||||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||||
|
|
||||||
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import { ConfigVariablesMetadataMap } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
import { ConfigVariablesMetadataMap } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
||||||
|
|
||||||
export interface ReflectMetadataTypeMap {
|
export interface ReflectMetadataTypeMap {
|
||||||
@ -16,6 +17,7 @@ export interface ReflectMetadataTypeMap {
|
|||||||
['workspace:duplicate-criteria-metadata-args']: WorkspaceEntityDuplicateCriteria[];
|
['workspace:duplicate-criteria-metadata-args']: WorkspaceEntityDuplicateCriteria[];
|
||||||
['config-variables']: ConfigVariablesMetadataMap;
|
['config-variables']: ConfigVariablesMetadataMap;
|
||||||
['workspace:is-searchable-metadata-args']: boolean;
|
['workspace:is-searchable-metadata-args']: boolean;
|
||||||
|
['feature-flag-metadata-args']: FeatureFlagKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypedReflect {
|
export class TypedReflect {
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@ -24,6 +24,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ai-sdk/anthropic@npm:^1.2.12":
|
||||||
|
version: 1.2.12
|
||||||
|
resolution: "@ai-sdk/anthropic@npm:1.2.12"
|
||||||
|
dependencies:
|
||||||
|
"@ai-sdk/provider": "npm:1.1.3"
|
||||||
|
"@ai-sdk/provider-utils": "npm:2.2.8"
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.0.0
|
||||||
|
checksum: 10c0/da13e1ed3c03efe207dbb0fd5fe9f399e4119e6687ec1096418a33a7eeea3c5f912a51c74b185bba3c203b15ee0c1b9cdf649711815ff8e769e31af266ac00fb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/openai@npm:^1.3.22":
|
"@ai-sdk/openai@npm:^1.3.22":
|
||||||
version: 1.3.22
|
version: 1.3.22
|
||||||
resolution: "@ai-sdk/openai@npm:1.3.22"
|
resolution: "@ai-sdk/openai@npm:1.3.22"
|
||||||
@ -56506,6 +56518,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "twenty-server@workspace:packages/twenty-server"
|
resolution: "twenty-server@workspace:packages/twenty-server"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@ai-sdk/anthropic": "npm:^1.2.12"
|
||||||
"@ai-sdk/openai": "npm:^1.3.22"
|
"@ai-sdk/openai": "npm:^1.3.22"
|
||||||
"@blocknote/server-util": "npm:^0.31.1"
|
"@blocknote/server-util": "npm:^0.31.1"
|
||||||
"@clickhouse/client": "npm:^1.11.0"
|
"@clickhouse/client": "npm:^1.11.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user