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;
|
||||
};
|
||||
|
||||
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 = {
|
||||
__typename?: 'Analytics';
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@ -320,8 +337,18 @@ export type CheckUserExistOutput = {
|
||||
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 = {
|
||||
__typename?: 'ClientConfig';
|
||||
aiModels: Array<ClientAiModelConfig>;
|
||||
analyticsEnabled: Scalars['Boolean']['output'];
|
||||
api: ApiConfig;
|
||||
authProviders: AuthProviders;
|
||||
@ -413,6 +440,14 @@ export type ConfigVariablesOutput = {
|
||||
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 = {
|
||||
expiresAt: Scalars['DateTime']['input'];
|
||||
};
|
||||
@ -944,6 +979,11 @@ export enum MessageChannelVisibility {
|
||||
SUBJECT = 'SUBJECT'
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
ANTHROPIC = 'ANTHROPIC',
|
||||
OPENAI = 'OPENAI'
|
||||
}
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateWorkflowVersion: Scalars['Boolean']['output'];
|
||||
@ -957,6 +997,7 @@ export type Mutation = {
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createObjectEvent: Analytics;
|
||||
createOneAgent: Agent;
|
||||
createOneAppToken: AppToken;
|
||||
createOneField: Field;
|
||||
createOneObject: Object;
|
||||
@ -969,6 +1010,7 @@ export type Mutation = {
|
||||
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteDatabaseConfigVariable: Scalars['Boolean']['output'];
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneField: Field;
|
||||
deleteOneObject: Object;
|
||||
deleteOneRemoteServer: RemoteServer;
|
||||
@ -1012,6 +1054,7 @@ export type Mutation = {
|
||||
unsyncRemoteTable: RemoteTable;
|
||||
updateDatabaseConfigVariable: Scalars['Boolean']['output'];
|
||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||
updateOneAgent: Agent;
|
||||
updateOneField: Field;
|
||||
updateOneObject: Object;
|
||||
updateOneRemoteServer: RemoteServer;
|
||||
@ -1093,6 +1136,11 @@ export type MutationCreateObjectEventArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneAgentArgs = {
|
||||
input: CreateAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneAppTokenArgs = {
|
||||
input: CreateOneAppTokenInput;
|
||||
};
|
||||
@ -1148,6 +1196,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneAgentArgs = {
|
||||
input: AgentIdInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneFieldArgs = {
|
||||
input: DeleteOneFieldInput;
|
||||
};
|
||||
@ -1339,6 +1392,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneAgentArgs = {
|
||||
input: UpdateAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneFieldArgs = {
|
||||
input: UpdateOneFieldMetadataInput;
|
||||
};
|
||||
@ -1655,8 +1713,10 @@ export type Query = {
|
||||
field: Field;
|
||||
fields: FieldConnection;
|
||||
findDistantTablesWithStatus: Array<RemoteTable>;
|
||||
findManyAgents: Array<Agent>;
|
||||
findManyRemoteServersByType: Array<RemoteServer>;
|
||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||
findOneAgent: Agent;
|
||||
findOneRemoteServerById: RemoteServer;
|
||||
findOneServerlessFunction: ServerlessFunction;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
@ -1726,6 +1786,11 @@ export type QueryFindManyRemoteServersByTypeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindOneAgentArgs = {
|
||||
input: AgentIdInput;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindOneRemoteServerByIdArgs = {
|
||||
input: RemoteServerIdInput;
|
||||
};
|
||||
@ -2280,6 +2345,15 @@ export type UuidFilterComparison = {
|
||||
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 = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
|
||||
@ -49,6 +49,23 @@ export type AdminPanelWorkerQueueHealth = {
|
||||
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 = {
|
||||
__typename?: 'Analytics';
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@ -312,8 +329,18 @@ export type CheckUserExistOutput = {
|
||||
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 = {
|
||||
__typename?: 'ClientConfig';
|
||||
aiModels: Array<ClientAiModelConfig>;
|
||||
analyticsEnabled: Scalars['Boolean'];
|
||||
api: ApiConfig;
|
||||
authProviders: AuthProviders;
|
||||
@ -405,6 +432,14 @@ export type ConfigVariablesOutput = {
|
||||
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 = {
|
||||
domain: Scalars['String'];
|
||||
email: Scalars['String'];
|
||||
@ -893,6 +928,11 @@ export enum MessageChannelVisibility {
|
||||
SUBJECT = 'SUBJECT'
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
ANTHROPIC = 'ANTHROPIC',
|
||||
OPENAI = 'OPENAI'
|
||||
}
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateWorkflowVersion: Scalars['Boolean'];
|
||||
@ -906,6 +946,7 @@ export type Mutation = {
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createObjectEvent: Analytics;
|
||||
createOneAgent: Agent;
|
||||
createOneAppToken: AppToken;
|
||||
createOneField: Field;
|
||||
createOneObject: Object;
|
||||
@ -917,6 +958,7 @@ export type Mutation = {
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneField: Field;
|
||||
deleteOneObject: Object;
|
||||
deleteOneRole: Scalars['String'];
|
||||
@ -956,6 +998,7 @@ export type Mutation = {
|
||||
trackAnalytics: Analytics;
|
||||
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||
updateOneAgent: Agent;
|
||||
updateOneField: Field;
|
||||
updateOneObject: Object;
|
||||
updateOneRole: Role;
|
||||
@ -1036,6 +1079,11 @@ export type MutationCreateObjectEventArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneAgentArgs = {
|
||||
input: CreateAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneFieldArgs = {
|
||||
input: CreateOneFieldMetadataInput;
|
||||
};
|
||||
@ -1076,6 +1124,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneAgentArgs = {
|
||||
input: AgentIdInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneFieldArgs = {
|
||||
input: DeleteOneFieldInput;
|
||||
};
|
||||
@ -1247,6 +1300,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneAgentArgs = {
|
||||
input: UpdateAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneFieldArgs = {
|
||||
input: UpdateOneFieldMetadataInput;
|
||||
};
|
||||
@ -1557,7 +1615,9 @@ export type Query = {
|
||||
currentWorkspace: Workspace;
|
||||
field: Field;
|
||||
fields: FieldConnection;
|
||||
findManyAgents: Array<Agent>;
|
||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||
findOneAgent: Agent;
|
||||
findOneServerlessFunction: ServerlessFunction;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||
@ -1605,6 +1665,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindOneAgentArgs = {
|
||||
input: AgentIdInput;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindOneServerlessFunctionArgs = {
|
||||
input: ServerlessFunctionIdInput;
|
||||
};
|
||||
@ -2118,6 +2183,15 @@ export type UuidFilterComparison = {
|
||||
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 = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
@ -2751,7 +2825,7 @@ export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredPro
|
||||
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<{
|
||||
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 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<{
|
||||
input: SubmitFormStepInput;
|
||||
}>;
|
||||
@ -4708,6 +4796,13 @@ export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult<GetMeteredPr
|
||||
export const GetClientConfigDocument = gql`
|
||||
query GetClientConfig {
|
||||
clientConfig {
|
||||
aiModels {
|
||||
modelId
|
||||
label
|
||||
provider
|
||||
inputCostPer1kTokensInCredits
|
||||
outputCostPer1kTokensInCredits
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
billingUrl
|
||||
@ -6426,6 +6521,84 @@ export function useUpdateWorkflowVersionStepMutation(baseOptions?: Apollo.Mutati
|
||||
export type UpdateWorkflowVersionStepMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionStepMutation>;
|
||||
export type UpdateWorkflowVersionStepMutationResult = Apollo.MutationResult<UpdateWorkflowVersionStepMutation>;
|
||||
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`
|
||||
mutation SubmitFormStep($input: SubmitFormStepInput!) {
|
||||
submitFormStep(input: $input)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
@ -29,6 +30,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
|
||||
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
|
||||
const setAuthProviders = useSetRecoilState(authProvidersState);
|
||||
const setAiModels = useSetRecoilState(aiModelsState);
|
||||
|
||||
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
|
||||
isDeveloperDefaultSignInPrefilledState,
|
||||
@ -134,6 +136,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
magicLink: false,
|
||||
sso: data?.clientConfig.authProviders.sso,
|
||||
});
|
||||
setAiModels(data?.clientConfig.aiModels || []);
|
||||
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
||||
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
|
||||
@ -197,6 +200,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
setIsAnalyticsEnabled,
|
||||
setDomainConfiguration,
|
||||
setAuthProviders,
|
||||
setAiModels,
|
||||
setCanManageFeatureFlags,
|
||||
setLabPublicFeatureFlags,
|
||||
setMicrosoftMessagingEnabled,
|
||||
|
||||
@ -3,6 +3,13 @@ import { gql } from '@apollo/client';
|
||||
export const GET_CLIENT_CONFIG = gql`
|
||||
query GetClientConfig {
|
||||
clientConfig {
|
||||
aiModels {
|
||||
modelId
|
||||
label
|
||||
provider
|
||||
inputCostPer1kTokensInCredits
|
||||
outputCostPer1kTokensInCredits
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
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 { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||
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 { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
|
||||
export const CommandMenuWorkflowSelectActionContent = ({
|
||||
workflow,
|
||||
@ -16,6 +16,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
const { createStep } = useCreateStep({
|
||||
workflow,
|
||||
});
|
||||
const filteredOtherActions = useFilteredOtherActions();
|
||||
|
||||
return (
|
||||
<RightDrawerStepListContainer>
|
||||
@ -33,7 +34,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
Other
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{OTHER_ACTIONS.map((action) => (
|
||||
{filteredOtherActions.map((action) => (
|
||||
<MenuItemCommand
|
||||
key={action.type}
|
||||
LeftIcon={getIcon(action.icon)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
workflowActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
workflowAiAgentActionSettingsSchema,
|
||||
workflowCodeActionSchema,
|
||||
workflowCodeActionSettingsSchema,
|
||||
workflowCreateRecordActionSchema,
|
||||
@ -72,7 +73,23 @@ export type WorkflowHttpRequestAction = z.infer<
|
||||
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 WorkflowStep = WorkflowAction;
|
||||
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
|
||||
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('CODE'),
|
||||
@ -177,6 +184,11 @@ export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({
|
||||
settings: workflowHttpRequestActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('AI_AGENT'),
|
||||
settings: workflowAiAgentActionSettingsSchema,
|
||||
});
|
||||
|
||||
// Combined action schema
|
||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowCodeActionSchema,
|
||||
@ -187,6 +199,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowFindRecordsActionSchema,
|
||||
workflowFormActionSchema,
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
]);
|
||||
|
||||
// Trigger schemas
|
||||
|
||||
@ -64,6 +64,13 @@ export const WorkflowDiagramStepNodeIcon = ({
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
case 'AI_AGENT': {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
<Icon size={theme.icon.size.md} color={theme.color.pink} />
|
||||
</StyledStepNodeLabelIconContainer>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<StyledStepNodeLabelIconContainer>
|
||||
|
||||
@ -2,6 +2,7 @@ import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
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 { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
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 { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
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 { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
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': {
|
||||
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',
|
||||
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`;
|
||||
case 'HTTP_REQUEST':
|
||||
return msg`HTTP Request`;
|
||||
case 'AI_AGENT':
|
||||
return msg`AI Agent`;
|
||||
|
||||
default:
|
||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ export const getActionIconColorOrThrow = ({
|
||||
return theme.font.color.tertiary;
|
||||
case 'SEND_EMAIL':
|
||||
return theme.color.blue;
|
||||
case 'AI_AGENT':
|
||||
return theme.color.pink;
|
||||
default:
|
||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ type Leaf = {
|
||||
type?: InputSchemaPropertyType;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
@ -14,6 +15,7 @@ type Node = {
|
||||
icon?: string;
|
||||
label?: string;
|
||||
value: OutputSchema;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type Link = {
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const mockedClientConfig: ClientConfig = {
|
||||
aiModels: [],
|
||||
signInPrefilled: true,
|
||||
isMultiWorkspaceEnabled: false,
|
||||
isEmailVerificationRequired: false,
|
||||
|
||||
Reference in New Issue
Block a user