From c3a9843fcb94653ae9e50a4578cddb37dfb9af35 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:29:28 +0530 Subject: [PATCH] AI settings tab (#13496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/user-attachments/assets/87f5a556-ff12-4ce0-aaa7-9120c0432151 --------- Co-authored-by: Félix Malfait --- .../src/generated-metadata/graphql.ts | 410 +++++++++++++++++- .../twenty-front/src/generated/graphql.ts | 72 +++ .../ai/graphql/fragments/agentFragment.ts | 18 + .../graphql/mutations/createAgentHandoff.ts | 7 + .../ai/graphql/mutations/createOneAgent.ts | 11 + .../ai/graphql/mutations/deleteOneAgent.ts | 11 + .../graphql/mutations/removeAgentHandoff.ts | 7 + .../ai/graphql/mutations/updateOneAgent.ts | 9 +- .../queries/findAgentHandoffTargets.ts | 18 + .../ai/graphql/queries/findAgentHandoffs.ts | 22 + .../ai/graphql/queries/findManyAgents.ts | 11 + .../ai/graphql/queries/findOneAgent.ts | 10 +- .../ai/hooks/useAgentRoleAssignment.ts | 51 --- .../ai/hooks/useAgentUpdateFormState.ts | 83 ---- .../modules/app/components/SettingsRoutes.tsx | 21 + .../SettingsNavigationDrawerItem.tsx | 2 + .../hooks/useSettingsNavigationItems.tsx | 14 +- .../src/modules/types/SettingsPath.ts | 3 + .../components/NavigationDrawerItem.tsx | 12 +- .../NavigationDrawerItem.stories.tsx | 60 ++- .../validation-schemas/workflowSchema.ts | 5 +- .../components/WorkflowEditActionAiAgent.tsx | 208 ++++----- .../constants/workflow-ai-agent-tabs.ts | 8 - .../workflowAiAgentSelectedRoleState.ts | 6 - .../src/pages/settings/ai/SettingsAI.tsx | 49 +++ .../pages/settings/ai/SettingsAgentForm.tsx | 244 +++++++++++ .../ai/components/SettingsAIAgentTableRow.tsx | 64 +++ .../ai/components/SettingsAIAgentsTable.tsx | 91 ++++ .../SettingsAgentDeleteConfirmationModal.tsx | 62 +++ .../SettingsAgentDetailSkeletonLoader.tsx | 84 ++++ .../components/SettingsAgentHandoffForm.tsx | 151 +++++++ .../SettingsAgentHandoffSection.tsx | 64 +++ .../components/SettingsAgentHandoffTable.tsx | 183 ++++++++ .../constants/SettingsAIAgentTableMetadata.ts | 25 ++ .../forms/components/SettingsAIAgentForm.tsx | 144 ++++++ .../ai/hooks/useSettingsAgentFormState.ts | 65 +++ .../settingsAIAgentFormSchema.ts | 17 + .../agent-role/agent-role.module.ts | 2 - .../agent/agent-execution.service.ts | 38 +- .../agent/agent-handoff-executor.service.ts | 27 +- .../agent/agent-handoff.service.ts | 18 + .../agent/agent-tool.service.ts | 75 ++-- .../metadata-modules/agent/agent.module.ts | 4 +- .../metadata-modules/agent/agent.resolver.ts | 101 ++++- .../metadata-modules/agent/agent.service.ts | 96 ++-- .../agent-handoff-description.const.ts | 2 + .../constants/agent-handoff-prompt.const.ts | 11 + .../constants/agent-system-prompts.const.ts | 24 +- .../agent/dtos/agent-handoff.dto.ts | 17 + .../metadata-modules/agent/dtos/agent.dto.ts | 20 +- .../agent/dtos/create-agent-handoff.input.ts | 15 + .../agent/dtos/create-agent.input.ts | 26 +- .../agent/dtos/remove-agent-handoff.input.ts | 10 + .../agent/dtos/update-agent.input.ts | 15 + .../workspace-manager.service.ts | 1 + ...workflow-version-step.workspace-service.ts | 30 +- .../ai-agent/ai-agent.workflow-action.ts | 24 +- .../workflow-ai-agent-action-input.type.ts | 3 +- .../suites/agent/agent.integration-spec.ts | 53 +++ .../agent/utils/agent-tool-test-utils.ts | 1 + 60 files changed, 2453 insertions(+), 482 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/graphql/fragments/agentFragment.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/mutations/createAgentHandoff.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/mutations/createOneAgent.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/mutations/deleteOneAgent.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/mutations/removeAgentHandoff.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffTargets.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffs.ts create mode 100644 packages/twenty-front/src/modules/ai/graphql/queries/findManyAgents.ts delete mode 100644 packages/twenty-front/src/modules/ai/hooks/useAgentRoleAssignment.ts delete mode 100644 packages/twenty-front/src/modules/ai/hooks/useAgentUpdateFormState.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/workflow-ai-agent-tabs.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentSelectedRoleState.ts create mode 100644 packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentTableRow.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentDeleteConfirmationModal.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentDetailSkeletonLoader.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentHandoffForm.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentHandoffSection.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentHandoffTable.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/constants/SettingsAIAgentTableMetadata.ts create mode 100644 packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx create mode 100644 packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAgentFormState.ts create mode 100644 packages/twenty-front/src/pages/settings/ai/validation-schemas/settingsAIAgentFormSchema.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-handoff-description.const.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-handoff-prompt.const.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/agent/dtos/agent-handoff.dto.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/agent/dtos/create-agent-handoff.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/agent/dtos/remove-agent-handoff.input.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index baa1311b49..6f90558610 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -53,7 +53,10 @@ export type Agent = { __typename?: 'Agent'; createdAt: Scalars['DateTime']; description?: Maybe; + icon?: Maybe; id: Scalars['UUID']; + isCustom: Scalars['Boolean']; + label: Scalars['String']; modelId: Scalars['String']; name: Scalars['String']; prompt: Scalars['String']; @@ -81,6 +84,13 @@ export type AgentChatThread = { updatedAt: Scalars['DateTime']; }; +export type AgentHandoffDto = { + __typename?: 'AgentHandoffDTO'; + description?: Maybe; + id: Scalars['UUID']; + toAgent: Agent; +}; + export type AgentIdInput = { /** The id of the agent. */ id: Scalars['UUID']; @@ -473,6 +483,23 @@ export type CreateAgentChatThreadInput = { agentId: Scalars['UUID']; }; +export type CreateAgentHandoffInput = { + description?: InputMaybe; + fromAgentId: Scalars['UUID']; + toAgentId: Scalars['UUID']; +}; + +export type CreateAgentInput = { + description?: InputMaybe; + icon?: InputMaybe; + label: Scalars['String']; + modelId: Scalars['String']; + name?: InputMaybe; + prompt: Scalars['String']; + responseFormat?: InputMaybe; + roleId?: InputMaybe; +}; + export type CreateApiKeyDto = { expiresAt: Scalars['String']; name: Scalars['String']; @@ -1123,6 +1150,7 @@ export type Mutation = { checkoutSession: BillingSessionOutput; computeStepOutputSchema: Scalars['JSON']; createAgentChatThread: AgentChatThread; + createAgentHandoff: Scalars['Boolean']; createApiKey: ApiKey; createApprovedAccessDomain: ApprovedAccessDomain; createDatabaseConfigVariable: Scalars['Boolean']; @@ -1130,6 +1158,7 @@ export type Mutation = { createFile: File; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; + createOneAgent: Agent; createOneAppToken: AppToken; createOneField: Field; createOneObject: Object; @@ -1144,6 +1173,7 @@ export type Mutation = { deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; deleteFile: File; + deleteOneAgent: Agent; deleteOneField: Field; deleteOneObject: Object; deleteOneRemoteServer: RemoteServer; @@ -1173,6 +1203,7 @@ export type Mutation = { initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; publishServerlessFunction: ServerlessFunction; + removeAgentHandoff: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewToken: AuthTokens; resendEmailVerificationToken: ResendEmailVerificationTokenOutput; @@ -1264,6 +1295,11 @@ export type MutationCreateAgentChatThreadArgs = { }; +export type MutationCreateAgentHandoffArgs = { + input: CreateAgentHandoffInput; +}; + + export type MutationCreateApiKeyArgs = { input: CreateApiKeyDto; }; @@ -1303,6 +1339,11 @@ export type MutationCreateObjectEventArgs = { }; +export type MutationCreateOneAgentArgs = { + input: CreateAgentInput; +}; + + export type MutationCreateOneAppTokenArgs = { input: CreateOneAppTokenInput; }; @@ -1368,6 +1409,11 @@ export type MutationDeleteFileArgs = { }; +export type MutationDeleteOneAgentArgs = { + input: AgentIdInput; +}; + + export type MutationDeleteOneFieldArgs = { input: DeleteOneFieldInput; }; @@ -1501,6 +1547,11 @@ export type MutationPublishServerlessFunctionArgs = { }; +export type MutationRemoveAgentHandoffArgs = { + input: RemoveAgentHandoffInput; +}; + + export type MutationRemoveRoleFromAgentArgs = { agentId: Scalars['UUID']; }; @@ -1990,7 +2041,10 @@ export type Query = { currentWorkspace: Workspace; field: Field; fields: FieldConnection; + findAgentHandoffTargets: Array; + findAgentHandoffs: Array; findDistantTablesWithStatus: Array; + findManyAgents: Array; findManyRemoteServersByType: Array; findManyServerlessFunctions: Array; findOneAgent: Agent; @@ -2078,6 +2132,16 @@ export type QueryFieldsArgs = { }; +export type QueryFindAgentHandoffTargetsArgs = { + input: AgentIdInput; +}; + + +export type QueryFindAgentHandoffsArgs = { + input: AgentIdInput; +}; + + export type QueryFindDistantTablesWithStatusArgs = { input: FindManyRemoteTablesInput; }; @@ -2313,6 +2377,11 @@ export enum RemoteTableStatus { SYNCED = 'SYNCED' } +export type RemoveAgentHandoffInput = { + fromAgentId: Scalars['String']; + toAgentId: Scalars['String']; +}; + export type ResendEmailVerificationTokenOutput = { __typename?: 'ResendEmailVerificationTokenOutput'; success: Scalars['Boolean']; @@ -2667,11 +2736,14 @@ export type UuidFilterComparison = { export type UpdateAgentInput = { description?: InputMaybe; + icon?: InputMaybe; id: Scalars['UUID']; + label: Scalars['String']; modelId: Scalars['String']; name: Scalars['String']; prompt: Scalars['String']; responseFormat?: InputMaybe; + roleId?: InputMaybe; }; export type UpdateApiKeyDto = { @@ -3088,6 +3160,8 @@ export type WorkspaceUrlsAndId = { workspaceUrls: WorkspaceUrls; }; +export type AgentFieldsFragment = { __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null, isCustom: boolean, createdAt: string, updatedAt: string }; + export type AssignRoleToAgentMutationVariables = Exact<{ agentId: Scalars['UUID']; roleId: Scalars['UUID']; @@ -3103,6 +3177,34 @@ export type CreateAgentChatThreadMutationVariables = Exact<{ export type CreateAgentChatThreadMutation = { __typename?: 'Mutation', createAgentChatThread: { __typename?: 'AgentChatThread', id: any, agentId: any, title?: string | null, createdAt: string, updatedAt: string } }; +export type CreateAgentHandoffMutationVariables = Exact<{ + input: CreateAgentHandoffInput; +}>; + + +export type CreateAgentHandoffMutation = { __typename?: 'Mutation', createAgentHandoff: boolean }; + +export type CreateOneAgentMutationVariables = Exact<{ + input: CreateAgentInput; +}>; + + +export type CreateOneAgentMutation = { __typename?: 'Mutation', createOneAgent: { __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null, isCustom: boolean, createdAt: string, updatedAt: string } }; + +export type DeleteOneAgentMutationVariables = Exact<{ + input: AgentIdInput; +}>; + + +export type DeleteOneAgentMutation = { __typename?: 'Mutation', deleteOneAgent: { __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null, isCustom: boolean, createdAt: string, updatedAt: string } }; + +export type RemoveAgentHandoffMutationVariables = Exact<{ + input: RemoveAgentHandoffInput; +}>; + + +export type RemoveAgentHandoffMutation = { __typename?: 'Mutation', removeAgentHandoff: boolean }; + export type RemoveRoleFromAgentMutationVariables = Exact<{ agentId: Scalars['UUID']; }>; @@ -3115,14 +3217,33 @@ export type UpdateOneAgentMutationVariables = Exact<{ }>; -export type UpdateOneAgentMutation = { __typename?: 'Mutation', updateOneAgent: { __typename?: 'Agent', id: any, name: string, description?: string | null, prompt: string, modelId: string, responseFormat?: any | null } }; +export type UpdateOneAgentMutation = { __typename?: 'Mutation', updateOneAgent: { __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null, isCustom: boolean, createdAt: string, updatedAt: string } }; + +export type FindAgentHandoffTargetsQueryVariables = Exact<{ + input: AgentIdInput; +}>; + + +export type FindAgentHandoffTargetsQuery = { __typename?: 'Query', findAgentHandoffTargets: Array<{ __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, modelId: string, prompt: string, isCustom: boolean, createdAt: string, updatedAt: string }> }; + +export type FindAgentHandoffsQueryVariables = Exact<{ + input: AgentIdInput; +}>; + + +export type FindAgentHandoffsQuery = { __typename?: 'Query', findAgentHandoffs: Array<{ __typename?: 'AgentHandoffDTO', id: any, description?: string | null, toAgent: { __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, modelId: string, prompt: string, isCustom: boolean, createdAt: string, updatedAt: string } }> }; + +export type FindManyAgentsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type FindManyAgentsQuery = { __typename?: 'Query', findManyAgents: Array<{ __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null, isCustom: boolean, createdAt: string, updatedAt: string }> }; 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, roleId?: any | null } }; +export type FindOneAgentQuery = { __typename?: 'Query', findOneAgent: { __typename?: 'Agent', id: any, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null, isCustom: boolean, createdAt: string, updatedAt: string } }; export type GetAgentChatMessagesQueryVariables = Exact<{ threadId: Scalars['String']; @@ -4064,6 +4185,22 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{ export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } }; +export const AgentFieldsFragmentDoc = gql` + fragment AgentFields on Agent { + id + name + label + description + icon + prompt + modelId + responseFormat + roleId + isCustom + createdAt + updatedAt +} + `; export const AuthTokenFragmentFragmentDoc = gql` fragment AuthTokenFragment on AuthToken { token @@ -4412,6 +4549,134 @@ export function useCreateAgentChatThreadMutation(baseOptions?: Apollo.MutationHo export type CreateAgentChatThreadMutationHookResult = ReturnType; export type CreateAgentChatThreadMutationResult = Apollo.MutationResult; export type CreateAgentChatThreadMutationOptions = Apollo.BaseMutationOptions; +export const CreateAgentHandoffDocument = gql` + mutation CreateAgentHandoff($input: CreateAgentHandoffInput!) { + createAgentHandoff(input: $input) +} + `; +export type CreateAgentHandoffMutationFn = Apollo.MutationFunction; + +/** + * __useCreateAgentHandoffMutation__ + * + * To run a mutation, you first call `useCreateAgentHandoffMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateAgentHandoffMutation` 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 [createAgentHandoffMutation, { data, loading, error }] = useCreateAgentHandoffMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateAgentHandoffMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateAgentHandoffDocument, options); + } +export type CreateAgentHandoffMutationHookResult = ReturnType; +export type CreateAgentHandoffMutationResult = Apollo.MutationResult; +export type CreateAgentHandoffMutationOptions = Apollo.BaseMutationOptions; +export const CreateOneAgentDocument = gql` + mutation CreateOneAgent($input: CreateAgentInput!) { + createOneAgent(input: $input) { + ...AgentFields + } +} + ${AgentFieldsFragmentDoc}`; +export type CreateOneAgentMutationFn = Apollo.MutationFunction; + +/** + * __useCreateOneAgentMutation__ + * + * To run a mutation, you first call `useCreateOneAgentMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateOneAgentMutation` 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 [createOneAgentMutation, { data, loading, error }] = useCreateOneAgentMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateOneAgentMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateOneAgentDocument, options); + } +export type CreateOneAgentMutationHookResult = ReturnType; +export type CreateOneAgentMutationResult = Apollo.MutationResult; +export type CreateOneAgentMutationOptions = Apollo.BaseMutationOptions; +export const DeleteOneAgentDocument = gql` + mutation DeleteOneAgent($input: AgentIdInput!) { + deleteOneAgent(input: $input) { + ...AgentFields + } +} + ${AgentFieldsFragmentDoc}`; +export type DeleteOneAgentMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteOneAgentMutation__ + * + * To run a mutation, you first call `useDeleteOneAgentMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteOneAgentMutation` 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 [deleteOneAgentMutation, { data, loading, error }] = useDeleteOneAgentMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useDeleteOneAgentMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteOneAgentDocument, options); + } +export type DeleteOneAgentMutationHookResult = ReturnType; +export type DeleteOneAgentMutationResult = Apollo.MutationResult; +export type DeleteOneAgentMutationOptions = Apollo.BaseMutationOptions; +export const RemoveAgentHandoffDocument = gql` + mutation RemoveAgentHandoff($input: RemoveAgentHandoffInput!) { + removeAgentHandoff(input: $input) +} + `; +export type RemoveAgentHandoffMutationFn = Apollo.MutationFunction; + +/** + * __useRemoveAgentHandoffMutation__ + * + * To run a mutation, you first call `useRemoveAgentHandoffMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveAgentHandoffMutation` 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 [removeAgentHandoffMutation, { data, loading, error }] = useRemoveAgentHandoffMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useRemoveAgentHandoffMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RemoveAgentHandoffDocument, options); + } +export type RemoveAgentHandoffMutationHookResult = ReturnType; +export type RemoveAgentHandoffMutationResult = Apollo.MutationResult; +export type RemoveAgentHandoffMutationOptions = Apollo.BaseMutationOptions; export const RemoveRoleFromAgentDocument = gql` mutation RemoveRoleFromAgent($agentId: UUID!) { removeRoleFromAgent(agentId: $agentId) @@ -4446,15 +4711,10 @@ export type RemoveRoleFromAgentMutationOptions = Apollo.BaseMutationOptions; /** @@ -4481,20 +4741,140 @@ export function useUpdateOneAgentMutation(baseOptions?: Apollo.MutationHookOptio export type UpdateOneAgentMutationHookResult = ReturnType; export type UpdateOneAgentMutationResult = Apollo.MutationResult; export type UpdateOneAgentMutationOptions = Apollo.BaseMutationOptions; -export const FindOneAgentDocument = gql` - query FindOneAgent($id: UUID!) { - findOneAgent(input: {id: $id}) { +export const FindAgentHandoffTargetsDocument = gql` + query FindAgentHandoffTargets($input: AgentIdInput!) { + findAgentHandoffTargets(input: $input) { id name + label description - prompt + icon modelId - responseFormat - roleId + prompt + isCustom + createdAt + updatedAt } } `; +/** + * __useFindAgentHandoffTargetsQuery__ + * + * To run a query within a React component, call `useFindAgentHandoffTargetsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindAgentHandoffTargetsQuery` 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 } = useFindAgentHandoffTargetsQuery({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useFindAgentHandoffTargetsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindAgentHandoffTargetsDocument, options); + } +export function useFindAgentHandoffTargetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindAgentHandoffTargetsDocument, options); + } +export type FindAgentHandoffTargetsQueryHookResult = ReturnType; +export type FindAgentHandoffTargetsLazyQueryHookResult = ReturnType; +export type FindAgentHandoffTargetsQueryResult = Apollo.QueryResult; +export const FindAgentHandoffsDocument = gql` + query FindAgentHandoffs($input: AgentIdInput!) { + findAgentHandoffs(input: $input) { + id + description + toAgent { + id + name + label + description + icon + modelId + prompt + isCustom + createdAt + updatedAt + } + } +} + `; + +/** + * __useFindAgentHandoffsQuery__ + * + * To run a query within a React component, call `useFindAgentHandoffsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindAgentHandoffsQuery` 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 } = useFindAgentHandoffsQuery({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useFindAgentHandoffsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindAgentHandoffsDocument, options); + } +export function useFindAgentHandoffsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindAgentHandoffsDocument, options); + } +export type FindAgentHandoffsQueryHookResult = ReturnType; +export type FindAgentHandoffsLazyQueryHookResult = ReturnType; +export type FindAgentHandoffsQueryResult = Apollo.QueryResult; +export const FindManyAgentsDocument = gql` + query FindManyAgents { + findManyAgents { + ...AgentFields + } +} + ${AgentFieldsFragmentDoc}`; + +/** + * __useFindManyAgentsQuery__ + * + * To run a query within a React component, call `useFindManyAgentsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindManyAgentsQuery` 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 } = useFindManyAgentsQuery({ + * variables: { + * }, + * }); + */ +export function useFindManyAgentsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindManyAgentsDocument, options); + } +export function useFindManyAgentsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindManyAgentsDocument, options); + } +export type FindManyAgentsQueryHookResult = ReturnType; +export type FindManyAgentsLazyQueryHookResult = ReturnType; +export type FindManyAgentsQueryResult = Apollo.QueryResult; +export const FindOneAgentDocument = gql` + query FindOneAgent($id: UUID!) { + findOneAgent(input: {id: $id}) { + ...AgentFields + } +} + ${AgentFieldsFragmentDoc}`; + /** * __useFindOneAgentQuery__ * diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index f50e67d789..d896a5589b 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -53,7 +53,10 @@ export type Agent = { __typename?: 'Agent'; createdAt: Scalars['DateTime']; description?: Maybe; + icon?: Maybe; id: Scalars['UUID']; + isCustom: Scalars['Boolean']; + label: Scalars['String']; modelId: Scalars['String']; name: Scalars['String']; prompt: Scalars['String']; @@ -81,6 +84,13 @@ export type AgentChatThread = { updatedAt: Scalars['DateTime']; }; +export type AgentHandoffDto = { + __typename?: 'AgentHandoffDTO'; + description?: Maybe; + id: Scalars['UUID']; + toAgent: Agent; +}; + export type AgentIdInput = { /** The id of the agent. */ id: Scalars['UUID']; @@ -473,6 +483,23 @@ export type CreateAgentChatThreadInput = { agentId: Scalars['UUID']; }; +export type CreateAgentHandoffInput = { + description?: InputMaybe; + fromAgentId: Scalars['UUID']; + toAgentId: Scalars['UUID']; +}; + +export type CreateAgentInput = { + description?: InputMaybe; + icon?: InputMaybe; + label: Scalars['String']; + modelId: Scalars['String']; + name?: InputMaybe; + prompt: Scalars['String']; + responseFormat?: InputMaybe; + roleId?: InputMaybe; +}; + export type CreateApiKeyDto = { expiresAt: Scalars['String']; name: Scalars['String']; @@ -1080,6 +1107,7 @@ export type Mutation = { checkoutSession: BillingSessionOutput; computeStepOutputSchema: Scalars['JSON']; createAgentChatThread: AgentChatThread; + createAgentHandoff: Scalars['Boolean']; createApiKey: ApiKey; createApprovedAccessDomain: ApprovedAccessDomain; createDatabaseConfigVariable: Scalars['Boolean']; @@ -1087,6 +1115,7 @@ export type Mutation = { createFile: File; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; + createOneAgent: Agent; createOneAppToken: AppToken; createOneField: Field; createOneObject: Object; @@ -1100,6 +1129,7 @@ export type Mutation = { deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; deleteFile: File; + deleteOneAgent: Agent; deleteOneField: Field; deleteOneObject: Object; deleteOneRole: Scalars['String']; @@ -1128,6 +1158,7 @@ export type Mutation = { initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; publishServerlessFunction: ServerlessFunction; + removeAgentHandoff: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewToken: AuthTokens; resendEmailVerificationToken: ResendEmailVerificationTokenOutput; @@ -1215,6 +1246,11 @@ export type MutationCreateAgentChatThreadArgs = { }; +export type MutationCreateAgentHandoffArgs = { + input: CreateAgentHandoffInput; +}; + + export type MutationCreateApiKeyArgs = { input: CreateApiKeyDto; }; @@ -1254,6 +1290,11 @@ export type MutationCreateObjectEventArgs = { }; +export type MutationCreateOneAgentArgs = { + input: CreateAgentInput; +}; + + export type MutationCreateOneFieldArgs = { input: CreateOneFieldMetadataInput; }; @@ -1304,6 +1345,11 @@ export type MutationDeleteFileArgs = { }; +export type MutationDeleteOneAgentArgs = { + input: AgentIdInput; +}; + + export type MutationDeleteOneFieldArgs = { input: DeleteOneFieldInput; }; @@ -1432,6 +1478,11 @@ export type MutationPublishServerlessFunctionArgs = { }; +export type MutationRemoveAgentHandoffArgs = { + input: RemoveAgentHandoffInput; +}; + + export type MutationRemoveRoleFromAgentArgs = { agentId: Scalars['UUID']; }; @@ -1901,6 +1952,9 @@ export type Query = { currentWorkspace: Workspace; field: Field; fields: FieldConnection; + findAgentHandoffTargets: Array; + findAgentHandoffs: Array; + findManyAgents: Array; findManyServerlessFunctions: Array; findOneAgent: Agent; findOneServerlessFunction: ServerlessFunction; @@ -1975,6 +2029,16 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = { }; +export type QueryFindAgentHandoffTargetsArgs = { + input: AgentIdInput; +}; + + +export type QueryFindAgentHandoffsArgs = { + input: AgentIdInput; +}; + + export type QueryFindOneAgentArgs = { input: AgentIdInput; }; @@ -2159,6 +2223,11 @@ export enum RemoteTableStatus { SYNCED = 'SYNCED' } +export type RemoveAgentHandoffInput = { + fromAgentId: Scalars['String']; + toAgentId: Scalars['String']; +}; + export type ResendEmailVerificationTokenOutput = { __typename?: 'ResendEmailVerificationTokenOutput'; success: Scalars['Boolean']; @@ -2513,11 +2582,14 @@ export type UuidFilterComparison = { export type UpdateAgentInput = { description?: InputMaybe; + icon?: InputMaybe; id: Scalars['UUID']; + label: Scalars['String']; modelId: Scalars['String']; name: Scalars['String']; prompt: Scalars['String']; responseFormat?: InputMaybe; + roleId?: InputMaybe; }; export type UpdateApiKeyDto = { diff --git a/packages/twenty-front/src/modules/ai/graphql/fragments/agentFragment.ts b/packages/twenty-front/src/modules/ai/graphql/fragments/agentFragment.ts new file mode 100644 index 0000000000..2f0afe4778 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/fragments/agentFragment.ts @@ -0,0 +1,18 @@ +import { gql } from '@apollo/client'; + +export const AGENT_FRAGMENT = gql` + fragment AgentFields on Agent { + id + name + label + description + icon + prompt + modelId + responseFormat + roleId + isCustom + createdAt + updatedAt + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/createAgentHandoff.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/createAgentHandoff.ts new file mode 100644 index 0000000000..31bbbdbde9 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/createAgentHandoff.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const CREATE_AGENT_HANDOFF = gql` + mutation CreateAgentHandoff($input: CreateAgentHandoffInput!) { + createAgentHandoff(input: $input) + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/createOneAgent.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/createOneAgent.ts new file mode 100644 index 0000000000..d48f46abae --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/createOneAgent.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; +import { AGENT_FRAGMENT } from '../fragments/agentFragment'; + +export const CREATE_ONE_AGENT = gql` + ${AGENT_FRAGMENT} + mutation CreateOneAgent($input: CreateAgentInput!) { + createOneAgent(input: $input) { + ...AgentFields + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/deleteOneAgent.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/deleteOneAgent.ts new file mode 100644 index 0000000000..02bb6db1f1 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/deleteOneAgent.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; +import { AGENT_FRAGMENT } from '../fragments/agentFragment'; + +export const DELETE_ONE_AGENT = gql` + ${AGENT_FRAGMENT} + mutation DeleteOneAgent($input: AgentIdInput!) { + deleteOneAgent(input: $input) { + ...AgentFields + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/removeAgentHandoff.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/removeAgentHandoff.ts new file mode 100644 index 0000000000..a317b61940 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/removeAgentHandoff.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const REMOVE_AGENT_HANDOFF = gql` + mutation RemoveAgentHandoff($input: RemoveAgentHandoffInput!) { + removeAgentHandoff(input: $input) + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/updateOneAgent.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/updateOneAgent.ts index 5797926007..66743dd26c 100644 --- a/packages/twenty-front/src/modules/ai/graphql/mutations/updateOneAgent.ts +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/updateOneAgent.ts @@ -1,14 +1,11 @@ import { gql } from '@apollo/client'; +import { AGENT_FRAGMENT } from '../fragments/agentFragment'; export const UPDATE_ONE_AGENT = gql` + ${AGENT_FRAGMENT} mutation UpdateOneAgent($input: UpdateAgentInput!) { updateOneAgent(input: $input) { - id - name - description - prompt - modelId - responseFormat + ...AgentFields } } `; diff --git a/packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffTargets.ts b/packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffTargets.ts new file mode 100644 index 0000000000..1fe34da9a3 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffTargets.ts @@ -0,0 +1,18 @@ +import { gql } from '@apollo/client'; + +export const FIND_AGENT_HANDOFF_TARGETS = gql` + query FindAgentHandoffTargets($input: AgentIdInput!) { + findAgentHandoffTargets(input: $input) { + id + name + label + description + icon + modelId + prompt + isCustom + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffs.ts b/packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffs.ts new file mode 100644 index 0000000000..45565d670d --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/queries/findAgentHandoffs.ts @@ -0,0 +1,22 @@ +import { gql } from '@apollo/client'; + +export const FIND_AGENT_HANDOFFS = gql` + query FindAgentHandoffs($input: AgentIdInput!) { + findAgentHandoffs(input: $input) { + id + description + toAgent { + id + name + label + description + icon + modelId + prompt + isCustom + createdAt + updatedAt + } + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/queries/findManyAgents.ts b/packages/twenty-front/src/modules/ai/graphql/queries/findManyAgents.ts new file mode 100644 index 0000000000..c7af91d570 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/queries/findManyAgents.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; +import { AGENT_FRAGMENT } from '../fragments/agentFragment'; + +export const FIND_MANY_AGENTS = gql` + ${AGENT_FRAGMENT} + query FindManyAgents { + findManyAgents { + ...AgentFields + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/queries/findOneAgent.ts b/packages/twenty-front/src/modules/ai/graphql/queries/findOneAgent.ts index 0798a8073c..1655c090cc 100644 --- a/packages/twenty-front/src/modules/ai/graphql/queries/findOneAgent.ts +++ b/packages/twenty-front/src/modules/ai/graphql/queries/findOneAgent.ts @@ -1,15 +1,11 @@ import { gql } from '@apollo/client'; +import { AGENT_FRAGMENT } from '../fragments/agentFragment'; export const FIND_ONE_AGENT = gql` + ${AGENT_FRAGMENT} query FindOneAgent($id: UUID!) { findOneAgent(input: { id: $id }) { - id - name - description - prompt - modelId - responseFormat - roleId + ...AgentFields } } `; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentRoleAssignment.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentRoleAssignment.ts deleted file mode 100644 index 3689a692f1..0000000000 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentRoleAssignment.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { workflowAiAgentSelectedRoleState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentSelectedRoleState'; -import { useRecoilState } from 'recoil'; -import { - useAssignRoleToAgentMutation, - useFindOneAgentQuery, - useGetRolesQuery, - useRemoveRoleFromAgentMutation, -} from '~/generated-metadata/graphql'; - -export const useAgentRoleAssignment = (agentId: string) => { - const [workflowAiAgentSelectedRole, setWorkflowAiAgentSelectedRole] = - useRecoilState(workflowAiAgentSelectedRoleState); - - useFindOneAgentQuery({ - variables: { id: agentId }, - skip: !agentId, - onCompleted: (data) => { - setWorkflowAiAgentSelectedRole(data.findOneAgent.roleId); - }, - }); - - const { data: rolesData } = useGetRolesQuery(); - const [assignRoleToAgent] = useAssignRoleToAgentMutation(); - const [removeRoleFromAgent] = useRemoveRoleFromAgentMutation(); - - const handleRoleChange = async (roleId: string) => { - if (roleId === '') { - await handleRoleRemove(); - } else { - setWorkflowAiAgentSelectedRole(roleId); - await assignRoleToAgent({ variables: { agentId, roleId } }); - } - }; - - const handleRoleRemove = async () => { - setWorkflowAiAgentSelectedRole(undefined); - await removeRoleFromAgent({ variables: { agentId } }); - }; - - const rolesOptions = - rolesData?.getRoles?.map((role) => ({ - label: role.label, - value: role.id, - })) || []; - - return { - selectedRole: workflowAiAgentSelectedRole, - handleRoleChange, - rolesOptions, - }; -}; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentUpdateFormState.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentUpdateFormState.ts deleted file mode 100644 index 09c200dbb2..0000000000 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentUpdateFormState.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { useDebouncedCallback } from 'use-debounce'; -import { - useFindOneAgentQuery, - useUpdateOneAgentMutation, -} from '~/generated-metadata/graphql'; - -type AgentFormValues = { - name: string; - prompt: string; - modelId: string; -}; - -export const useAgentUpdateFormState = ({ - agentId, - readonly = false, -}: { - agentId: string; - readonly?: boolean; -}) => { - const [formValues, setFormValues] = useState({ - name: '', - prompt: '', - modelId: '', - }); - - const { loading } = useFindOneAgentQuery({ - 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] = useUpdateOneAgentMutation(); - - const updateAgentMutation = async (updates: 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, - }; -}; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index f8db3e5905..6ca1a4244b 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -146,6 +146,18 @@ const SettingsWorkspace = lazy(() => })), ); +const SettingsAI = lazy(() => + import('~/pages/settings/ai/SettingsAI').then((module) => ({ + default: module.SettingsAI, + })), +); + +const SettingsAgentForm = lazy(() => + import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({ + default: module.SettingsAgentForm, + })), +); + const SettingsDomain = lazy(() => import('~/pages/settings/workspace/SettingsDomain').then((module) => ({ default: module.SettingsDomain, @@ -410,6 +422,15 @@ export const SettingsRoutes = ({ } > } /> + } /> + } + /> + } + /> } /> } /> diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItem.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItem.tsx index f463e60a23..9b46688337 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItem.tsx @@ -38,6 +38,7 @@ export const SettingsNavigationDrawerItem = ({ Icon={item.Icon} active={isActive} soon={item.soon} + isNew={item.isNew} onClick={item.onClick} /> @@ -53,6 +54,7 @@ export const SettingsNavigationDrawerItem = ({ Icon={item.Icon} active={isActive} soon={item.soon} + isNew={item.isNew} onClick={item.onClick} /> ); diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx index 99154fecc3..d792ea912b 100644 --- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx @@ -6,6 +6,7 @@ import { billingState } from '@/client-config/states/billingState'; import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState'; import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap'; import { NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { t } from '@lingui/core/macro'; import { useRecoilValue } from 'recoil'; import { @@ -26,11 +27,12 @@ import { IconRocket, IconServer, IconSettings, + IconSparkles, IconUserCircle, IconUsers, IconWebhook, } from 'twenty-ui/display'; -import { PermissionFlagType } from '~/generated/graphql'; +import { FeatureFlagKey, PermissionFlagType } from '~/generated/graphql'; export type SettingsNavigationSection = { label: string; @@ -49,6 +51,7 @@ export type SettingsNavigationItem = { subItems?: SettingsNavigationItem[]; isAdvanced?: boolean; soon?: boolean; + isNew?: boolean; }; const useSettingsNavigationItems = (): SettingsNavigationSection[] => { @@ -62,6 +65,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { (currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ?? false; const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState); + const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED); const permissionMap = usePermissionFlagMap(); return [ @@ -140,6 +144,14 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { Icon: IconApps, isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS], }, + { + label: t`AI`, + path: SettingsPath.AI, + Icon: IconSparkles, + isHidden: + !isAIEnabled || !permissionMap[PermissionFlagType.WORKSPACE], + isNew: true, + }, { label: t`Security`, path: SettingsPath.Security, diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts index bc9d7c7acf..13dff0e3de 100644 --- a/packages/twenty-front/src/modules/types/SettingsPath.ts +++ b/packages/twenty-front/src/modules/types/SettingsPath.ts @@ -22,6 +22,9 @@ export enum SettingsPath { WorkspaceMembersPage = 'members', Workspace = 'general', Domain = 'general/domain', + AI = 'ai', + AINewAgent = 'ai/new-agent', + AIAgentDetail = 'ai/agents/:agentId', APIs = 'apis', RestPlayground = 'playground/rest/:schema', GraphQLPlayground = 'playground/graphql/:schema', diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index 4896c8b57e..c9779b16a0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -33,6 +33,7 @@ export type NavigationDrawerItemProps = { active?: boolean; danger?: boolean; soon?: boolean; + isNew?: boolean; count?: number; keyboard?: string[]; rightOptions?: ReactNode; @@ -247,6 +248,7 @@ export const NavigationDrawerItem = ({ active, danger, soon, + isNew, count, keyboard, subItemState, @@ -262,7 +264,9 @@ export const NavigationDrawerItem = ({ const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = useRecoilState(isNavigationDrawerExpandedState); const showBreadcrumb = indentationLevel === 2; - const showStyledSpacer = !!soon || !!count || !!keyboard || !!rightOptions; + const showStyledSpacer = Boolean( + soon || isNew || count || keyboard || rightOptions, + ); const handleMobileNavigation = () => { if (isMobile) { @@ -343,6 +347,12 @@ export const NavigationDrawerItem = ({ )} + {isNew && ( + + + + )} + {!!count && ( {count} diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx index e4d2aa41ab..425f6e034f 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawerItem.stories.tsx @@ -1,12 +1,12 @@ import styled from '@emotion/styled'; import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from 'twenty-ui/display'; +import { CatalogDecorator, CatalogStory } from 'twenty-ui/testing'; +import { getOsControlSymbol } from 'twenty-ui/utilities'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { NavigationDrawerItem } from '../NavigationDrawerItem'; -import { CatalogDecorator, CatalogStory } from 'twenty-ui/testing'; -import { IconSearch } from 'twenty-ui/display'; -import { getOsControlSymbol } from 'twenty-ui/utilities'; const StyledContainer = styled.div` display: flex; @@ -96,6 +96,40 @@ export const Breadcrumb: Story = { ], }; +export const NewPill: Story = { + decorators: [ + (Story) => ( + +

New Pill Examples

+ + + +
+ ), + ComponentWithRouterDecorator, + ], +}; + export const BreadcrumbCatalog: CatalogStory< Story, typeof NavigationDrawerItem @@ -182,15 +216,23 @@ export const Catalog: CatalogStory = { }, { name: 'adornments', - values: ['Without Adornments', 'Soon Pill', 'Count', 'Keyboard Keys'], + values: [ + 'Without Adornments', + 'Soon Pill', + 'New Pill', + 'Count', + 'Keyboard Keys', + ], props: (adornmentName: string) => adornmentName === 'Soon Pill' ? { soon: true } - : adornmentName === 'Count' - ? { count: 3 } - : adornmentName === 'Keyboard Keys' - ? { keyboard: [getOsControlSymbol(), 'K'] } - : {}, + : adornmentName === 'New Pill' + ? { isNew: true } + : adornmentName === 'Count' + ? { count: 3 } + : adornmentName === 'Keyboard Keys' + ? { keyboard: [getOsControlSymbol(), 'K'] } + : {}, }, ], }, diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts index 18788442cf..a63367b55b 100644 --- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts +++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts @@ -1,6 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { z } from 'zod'; import { StepStatus } from 'twenty-shared/workflow'; +import { z } from 'zod'; // Base schemas export const objectRecordSchema = z.record(z.any()); @@ -135,7 +135,8 @@ export const workflowHttpRequestActionSettingsSchema = export const workflowAiAgentActionSettingsSchema = baseWorkflowActionSettingsSchema.extend({ input: z.object({ - agentId: z.string(), + agentId: z.string().optional(), + prompt: z.string().optional(), }), }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx index 7f53a4f836..5ea29f125d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx @@ -1,32 +1,20 @@ +import { useAiAgentOutputSchema } from '@/ai/hooks/useAiAgentOutputSchema'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; import { Select } from '@/ui/input/components/Select'; -import { TabList } from '@/ui/layout/tab-list/components/TabList'; -import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { WorkflowAiAgentAction } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; -import { AIChatTab } from '@/ai/components/AIChatTab'; 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 { - IconMessage, - IconSettings, - IconTool, - useIcons, -} from 'twenty-ui/display'; +import { useRecoilValue } from 'recoil'; +import { useIcons } from 'twenty-ui/display'; +import { SelectOption } from 'twenty-ui/input'; +import { useFindManyAgentsQuery } from '~/generated-metadata/graphql'; import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader'; -import { - WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID, - WorkflowAiAgentTabId, -} from '../constants/workflow-ai-agent-tabs'; -import { useAgentRoleAssignment } from '@/ai/hooks/useAgentRoleAssignment'; -import { useAgentUpdateFormState } from '@/ai/hooks/useAgentUpdateFormState'; -import { useAiAgentOutputSchema } from '@/ai/hooks/useAiAgentOutputSchema'; -import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions'; import { WorkflowOutputSchemaBuilder } from './WorkflowOutputSchemaBuilder'; const StyledErrorMessage = styled.div` @@ -36,11 +24,6 @@ const StyledErrorMessage = styled.div` margin-top: ${({ theme }) => theme.spacing(1)}; `; -const StyledTabList = styled(TabList)` - background-color: ${({ theme }) => theme.background.secondary}; - padding-left: ${({ theme }) => theme.spacing(2)}; -`; - type WorkflowEditActionAiAgentProps = { action: WorkflowAiAgentAction; actionOptions: @@ -55,6 +38,7 @@ export const WorkflowEditActionAiAgent = ({ action, actionOptions, }: WorkflowEditActionAiAgentProps) => { + const currentWorkspace = useRecoilValue(currentWorkspaceState); const { getIcon } = useIcons(); const { headerTitle, headerIcon, headerIconColor, headerType } = useWorkflowActionHeader({ @@ -62,13 +46,6 @@ export const WorkflowEditActionAiAgent = ({ defaultTitle: 'AI Agent', }); - const agentId = action.settings.input.agentId; - - const { formValues, handleFieldChange, loading } = useAgentUpdateFormState({ - agentId, - readonly: actionOptions.readonly === true, - }); - const { handleOutputSchemaChange, outputFields } = useAiAgentOutputSchema( action.settings.outputSchema as BaseOutputSchema, actionOptions.readonly === true ? undefined : actionOptions.onActionUpdate, @@ -76,107 +53,98 @@ export const WorkflowEditActionAiAgent = ({ actionOptions.readonly, ); - const modelOptions = useAiModelOptions(); + const { data: agentsData, loading: agentsLoading } = useFindManyAgentsQuery(); - const noModelsAvailable = modelOptions.length === 0; - - const activeTabId = useRecoilComponentValueV2( - activeTabIdComponentState, - WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID, + const agentOptions = (agentsData?.findManyAgents || []).reduce< + SelectOption[] + >( + (acc, agent) => { + if (agent.id !== currentWorkspace?.defaultAgent?.id) { + acc.push({ + label: agent.label, + value: agent.id, + Icon: agent.icon ? getIcon(agent.icon) : undefined, + }); + } + return acc; + }, + [ + { + label: t`No Agent`, + value: '', + }, + ], ); - const { rolesOptions, selectedRole, handleRoleChange } = - useAgentRoleAssignment(agentId); + const noAgentsAvailable = agentOptions.length === 0; - const tabs = [ - { - id: WorkflowAiAgentTabId.SETTINGS, - title: t`Settings`, - Icon: IconSettings, - }, - { id: WorkflowAiAgentTabId.TOOLS, title: t`Tools`, Icon: IconTool }, - { id: WorkflowAiAgentTabId.CHAT, title: t`Chat`, Icon: IconMessage }, - ]; + const handleFieldChange = (field: 'agentId' | 'prompt', value: string) => { + if (actionOptions.readonly === true) { + return; + } + actionOptions.onActionUpdate?.({ + ...action, + settings: { + ...action.settings, + input: { + ...action.settings.input, + [field]: value, + }, + }, + }); + }; - return loading ? ( + return agentsLoading ? ( ) : ( <> - { + if (actionOptions.readonly === true) { + return; + } + actionOptions.onActionUpdate?.({ ...action, name: newName }); + }} + Icon={getIcon(headerIcon)} + iconColor={headerIconColor} + initialTitle={headerTitle} + headerType={headerType} + disabled={actionOptions.readonly} /> - {activeTabId === WorkflowAiAgentTabId.CHAT ? ( - - ) : ( - <> - { - if (actionOptions.readonly === true) { - return; - } - actionOptions.onActionUpdate?.({ ...action, name: newName }); - }} - Icon={getIcon(headerIcon)} - iconColor={headerIconColor} - initialTitle={headerTitle} - headerType={headerType} - disabled={actionOptions.readonly} + +
+ handleFieldChange('modelId', value)} - disabled={actionOptions.readonly || noModelsAvailable} - emptyOption={{ - label: t`Auto`, - value: '', - }} - /> - {noModelsAvailable && ( - - {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.`} - - )} -
- handleFieldChange('prompt', value)} - VariablePicker={WorkflowVariablePicker} - multiline - /> - - - )} - {activeTabId === WorkflowAiAgentTabId.TOOLS && ( - + +