AI settings tab (#13496)

https://github.com/user-attachments/assets/87f5a556-ff12-4ce0-aaa7-9120c0432151

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Abdul Rahman
2025-08-01 20:29:28 +05:30
committed by GitHub
parent 75214c3e61
commit c3a9843fcb
60 changed files with 2453 additions and 482 deletions
@@ -53,7 +53,10 @@ export type Agent = {
__typename?: 'Agent';
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
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<Scalars['String']>;
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<Scalars['String']>;
fromAgentId: Scalars['UUID'];
toAgentId: Scalars['UUID'];
};
export type CreateAgentInput = {
description?: InputMaybe<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
label: Scalars['String'];
modelId: Scalars['String'];
name?: InputMaybe<Scalars['String']>;
prompt: Scalars['String'];
responseFormat?: InputMaybe<Scalars['JSON']>;
roleId?: InputMaybe<Scalars['UUID']>;
};
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<Agent>;
findAgentHandoffs: Array<AgentHandoffDto>;
findDistantTablesWithStatus: Array<RemoteTable>;
findManyAgents: Array<Agent>;
findManyRemoteServersByType: Array<RemoteServer>;
findManyServerlessFunctions: Array<ServerlessFunction>;
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<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
id: Scalars['UUID'];
label: Scalars['String'];
modelId: Scalars['String'];
name: Scalars['String'];
prompt: Scalars['String'];
responseFormat?: InputMaybe<Scalars['JSON']>;
roleId?: InputMaybe<Scalars['UUID']>;
};
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<typeof useCreateAgentChatThreadMutation>;
export type CreateAgentChatThreadMutationResult = Apollo.MutationResult<CreateAgentChatThreadMutation>;
export type CreateAgentChatThreadMutationOptions = Apollo.BaseMutationOptions<CreateAgentChatThreadMutation, CreateAgentChatThreadMutationVariables>;
export const CreateAgentHandoffDocument = gql`
mutation CreateAgentHandoff($input: CreateAgentHandoffInput!) {
createAgentHandoff(input: $input)
}
`;
export type CreateAgentHandoffMutationFn = Apollo.MutationFunction<CreateAgentHandoffMutation, CreateAgentHandoffMutationVariables>;
/**
* __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<CreateAgentHandoffMutation, CreateAgentHandoffMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateAgentHandoffMutation, CreateAgentHandoffMutationVariables>(CreateAgentHandoffDocument, options);
}
export type CreateAgentHandoffMutationHookResult = ReturnType<typeof useCreateAgentHandoffMutation>;
export type CreateAgentHandoffMutationResult = Apollo.MutationResult<CreateAgentHandoffMutation>;
export type CreateAgentHandoffMutationOptions = Apollo.BaseMutationOptions<CreateAgentHandoffMutation, CreateAgentHandoffMutationVariables>;
export const CreateOneAgentDocument = gql`
mutation CreateOneAgent($input: CreateAgentInput!) {
createOneAgent(input: $input) {
...AgentFields
}
}
${AgentFieldsFragmentDoc}`;
export type CreateOneAgentMutationFn = Apollo.MutationFunction<CreateOneAgentMutation, CreateOneAgentMutationVariables>;
/**
* __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<CreateOneAgentMutation, CreateOneAgentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateOneAgentMutation, CreateOneAgentMutationVariables>(CreateOneAgentDocument, options);
}
export type CreateOneAgentMutationHookResult = ReturnType<typeof useCreateOneAgentMutation>;
export type CreateOneAgentMutationResult = Apollo.MutationResult<CreateOneAgentMutation>;
export type CreateOneAgentMutationOptions = Apollo.BaseMutationOptions<CreateOneAgentMutation, CreateOneAgentMutationVariables>;
export const DeleteOneAgentDocument = gql`
mutation DeleteOneAgent($input: AgentIdInput!) {
deleteOneAgent(input: $input) {
...AgentFields
}
}
${AgentFieldsFragmentDoc}`;
export type DeleteOneAgentMutationFn = Apollo.MutationFunction<DeleteOneAgentMutation, DeleteOneAgentMutationVariables>;
/**
* __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<DeleteOneAgentMutation, DeleteOneAgentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteOneAgentMutation, DeleteOneAgentMutationVariables>(DeleteOneAgentDocument, options);
}
export type DeleteOneAgentMutationHookResult = ReturnType<typeof useDeleteOneAgentMutation>;
export type DeleteOneAgentMutationResult = Apollo.MutationResult<DeleteOneAgentMutation>;
export type DeleteOneAgentMutationOptions = Apollo.BaseMutationOptions<DeleteOneAgentMutation, DeleteOneAgentMutationVariables>;
export const RemoveAgentHandoffDocument = gql`
mutation RemoveAgentHandoff($input: RemoveAgentHandoffInput!) {
removeAgentHandoff(input: $input)
}
`;
export type RemoveAgentHandoffMutationFn = Apollo.MutationFunction<RemoveAgentHandoffMutation, RemoveAgentHandoffMutationVariables>;
/**
* __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<RemoveAgentHandoffMutation, RemoveAgentHandoffMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveAgentHandoffMutation, RemoveAgentHandoffMutationVariables>(RemoveAgentHandoffDocument, options);
}
export type RemoveAgentHandoffMutationHookResult = ReturnType<typeof useRemoveAgentHandoffMutation>;
export type RemoveAgentHandoffMutationResult = Apollo.MutationResult<RemoveAgentHandoffMutation>;
export type RemoveAgentHandoffMutationOptions = Apollo.BaseMutationOptions<RemoveAgentHandoffMutation, RemoveAgentHandoffMutationVariables>;
export const RemoveRoleFromAgentDocument = gql`
mutation RemoveRoleFromAgent($agentId: UUID!) {
removeRoleFromAgent(agentId: $agentId)
@@ -4446,15 +4711,10 @@ export type RemoveRoleFromAgentMutationOptions = Apollo.BaseMutationOptions<Remo
export const UpdateOneAgentDocument = gql`
mutation UpdateOneAgent($input: UpdateAgentInput!) {
updateOneAgent(input: $input) {
id
name
description
prompt
modelId
responseFormat
...AgentFields
}
}
`;
${AgentFieldsFragmentDoc}`;
export type UpdateOneAgentMutationFn = Apollo.MutationFunction<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>;
/**
@@ -4481,20 +4741,140 @@ export function useUpdateOneAgentMutation(baseOptions?: Apollo.MutationHookOptio
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}) {
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<FindAgentHandoffTargetsQuery, FindAgentHandoffTargetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindAgentHandoffTargetsQuery, FindAgentHandoffTargetsQueryVariables>(FindAgentHandoffTargetsDocument, options);
}
export function useFindAgentHandoffTargetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindAgentHandoffTargetsQuery, FindAgentHandoffTargetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindAgentHandoffTargetsQuery, FindAgentHandoffTargetsQueryVariables>(FindAgentHandoffTargetsDocument, options);
}
export type FindAgentHandoffTargetsQueryHookResult = ReturnType<typeof useFindAgentHandoffTargetsQuery>;
export type FindAgentHandoffTargetsLazyQueryHookResult = ReturnType<typeof useFindAgentHandoffTargetsLazyQuery>;
export type FindAgentHandoffTargetsQueryResult = Apollo.QueryResult<FindAgentHandoffTargetsQuery, FindAgentHandoffTargetsQueryVariables>;
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<FindAgentHandoffsQuery, FindAgentHandoffsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindAgentHandoffsQuery, FindAgentHandoffsQueryVariables>(FindAgentHandoffsDocument, options);
}
export function useFindAgentHandoffsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindAgentHandoffsQuery, FindAgentHandoffsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindAgentHandoffsQuery, FindAgentHandoffsQueryVariables>(FindAgentHandoffsDocument, options);
}
export type FindAgentHandoffsQueryHookResult = ReturnType<typeof useFindAgentHandoffsQuery>;
export type FindAgentHandoffsLazyQueryHookResult = ReturnType<typeof useFindAgentHandoffsLazyQuery>;
export type FindAgentHandoffsQueryResult = Apollo.QueryResult<FindAgentHandoffsQuery, FindAgentHandoffsQueryVariables>;
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<FindManyAgentsQuery, FindManyAgentsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindManyAgentsQuery, FindManyAgentsQueryVariables>(FindManyAgentsDocument, options);
}
export function useFindManyAgentsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindManyAgentsQuery, FindManyAgentsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindManyAgentsQuery, FindManyAgentsQueryVariables>(FindManyAgentsDocument, options);
}
export type FindManyAgentsQueryHookResult = ReturnType<typeof useFindManyAgentsQuery>;
export type FindManyAgentsLazyQueryHookResult = ReturnType<typeof useFindManyAgentsLazyQuery>;
export type FindManyAgentsQueryResult = Apollo.QueryResult<FindManyAgentsQuery, FindManyAgentsQueryVariables>;
export const FindOneAgentDocument = gql`
query FindOneAgent($id: UUID!) {
findOneAgent(input: {id: $id}) {
...AgentFields
}
}
${AgentFieldsFragmentDoc}`;
/**
* __useFindOneAgentQuery__
*
@@ -53,7 +53,10 @@ export type Agent = {
__typename?: 'Agent';
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
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<Scalars['String']>;
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<Scalars['String']>;
fromAgentId: Scalars['UUID'];
toAgentId: Scalars['UUID'];
};
export type CreateAgentInput = {
description?: InputMaybe<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
label: Scalars['String'];
modelId: Scalars['String'];
name?: InputMaybe<Scalars['String']>;
prompt: Scalars['String'];
responseFormat?: InputMaybe<Scalars['JSON']>;
roleId?: InputMaybe<Scalars['UUID']>;
};
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<Agent>;
findAgentHandoffs: Array<AgentHandoffDto>;
findManyAgents: Array<Agent>;
findManyServerlessFunctions: Array<ServerlessFunction>;
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<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
id: Scalars['UUID'];
label: Scalars['String'];
modelId: Scalars['String'];
name: Scalars['String'];
prompt: Scalars['String'];
responseFormat?: InputMaybe<Scalars['JSON']>;
roleId?: InputMaybe<Scalars['UUID']>;
};
export type UpdateApiKeyDto = {
@@ -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
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const CREATE_AGENT_HANDOFF = gql`
mutation CreateAgentHandoff($input: CreateAgentHandoffInput!) {
createAgentHandoff(input: $input)
}
`;
@@ -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
}
}
`;
@@ -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
}
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const REMOVE_AGENT_HANDOFF = gql`
mutation RemoveAgentHandoff($input: RemoveAgentHandoffInput!) {
removeAgentHandoff(input: $input)
}
`;
@@ -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
}
}
`;
@@ -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
}
}
`;
@@ -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
}
}
}
`;
@@ -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
}
}
`;
@@ -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
}
}
`;
@@ -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,
};
};
@@ -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<AgentFormValues>({
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,
};
};
@@ -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 = ({
}
>
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
<Route path={SettingsPath.AI} element={<SettingsAI />} />
<Route
path={SettingsPath.AINewAgent}
element={<SettingsAgentForm mode="create" />}
/>
<Route
path={SettingsPath.AIAgentDetail}
element={<SettingsAgentForm mode="edit" />}
/>
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
<Route path={SettingsPath.Domain} element={<SettingsDomain />} />
</Route>
@@ -38,6 +38,7 @@ export const SettingsNavigationDrawerItem = ({
Icon={item.Icon}
active={isActive}
soon={item.soon}
isNew={item.isNew}
onClick={item.onClick}
/>
</AdvancedSettingsWrapper>
@@ -53,6 +54,7 @@ export const SettingsNavigationDrawerItem = ({
Icon={item.Icon}
active={isActive}
soon={item.soon}
isNew={item.isNew}
onClick={item.onClick}
/>
);
@@ -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,
@@ -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',
@@ -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 = ({
</NavigationDrawerAnimatedCollapseWrapper>
)}
{isNew && (
<NavigationDrawerAnimatedCollapseWrapper>
<Pill label="New" />
</NavigationDrawerAnimatedCollapseWrapper>
)}
{!!count && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledItemCount>{count}</StyledItemCount>
@@ -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) => (
<StyledContainer>
<h1>New Pill Examples</h1>
<Story
args={{
label: 'New Feature',
Icon: IconSearch,
isNew: true,
}}
/>
<Story
args={{
label: 'New Feature with Count',
Icon: IconSearch,
isNew: true,
count: 5,
}}
/>
<Story
args={{
label: 'New Feature with Keyboard Shortcut',
Icon: IconSearch,
isNew: true,
keyboard: [getOsControlSymbol(), 'N'],
}}
/>
</StyledContainer>
),
ComponentWithRouterDecorator,
],
};
export const BreadcrumbCatalog: CatalogStory<
Story,
typeof NavigationDrawerItem
@@ -182,15 +216,23 @@ export const Catalog: CatalogStory<Story, typeof NavigationDrawerItem> = {
},
{
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'] }
: {},
},
],
},
@@ -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(),
}),
});
@@ -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<string>[]
>(
(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 ? (
<RightDrawerSkeletonLoader />
) : (
<>
<StyledTabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID}
<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}
/>
{activeTabId === WorkflowAiAgentTabId.CHAT ? (
<AIChatTab agentId={agentId} isWorkflowAgentNodeChat />
) : (
<>
<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-agent"
label={t`Select Agent`}
options={agentOptions}
value={action.settings.input.agentId || ''}
onChange={(value) => handleFieldChange('agentId', value)}
disabled={actionOptions.readonly || noAgentsAvailable}
/>
<WorkflowStepBody>
{activeTabId === WorkflowAiAgentTabId.SETTINGS && (
<>
<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`Auto`,
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}
/>
</>
)}
{activeTabId === WorkflowAiAgentTabId.TOOLS && (
<Select
dropdownId="select-agent-role"
label={t`Assign Role`}
options={[{ label: t`No role`, value: '' }, ...rolesOptions]}
value={selectedRole || ''}
onChange={handleRoleChange}
disabled={actionOptions.readonly}
/>
)}
</WorkflowStepBody>
</>
)}
{noAgentsAvailable && (
<StyledErrorMessage>
{t`Please create agents in the AI settings to use in workflows.`}
</StyledErrorMessage>
)}
</div>
<FormTextFieldInput
multiline
VariablePicker={WorkflowVariablePicker}
label={t`Instructions for AI`}
placeholder={t`Describe what you want the AI to do...`}
defaultValue={action.settings.input.prompt}
onChange={(value) => handleFieldChange('prompt', value)}
readonly={actionOptions.readonly}
/>
<WorkflowOutputSchemaBuilder
fields={outputFields}
onChange={handleOutputSchemaChange}
readonly={actionOptions.readonly}
/>
</WorkflowStepBody>
</>
);
};
@@ -1,8 +0,0 @@
export enum WorkflowAiAgentTabId {
SETTINGS = 'settings',
TOOLS = 'tools',
CHAT = 'chat',
}
export const WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID =
'WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID';
@@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const workflowAiAgentSelectedRoleState = atom<string | undefined>({
key: 'workflowAiAgentSelectedRoleState',
default: undefined,
});
@@ -0,0 +1,49 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { useFindManyAgentsQuery } from '~/generated-metadata/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { t } from '@lingui/core/macro';
import { SettingsAIAgentsTable } from './components/SettingsAIAgentsTable';
export const SettingsAI = () => {
const { data } = useFindManyAgentsQuery();
return (
<SubMenuTopBarContainer
title={t`AI`}
actionButton={
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewAgent)}>
<Button
Icon={IconPlus}
title={t`New Agent`}
accent="blue"
size="small"
/>
</UndecoratedLink>
}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: t`AI` },
]}
>
<SettingsPageContainer>
<Section>
<H2Title
title={t`Agents`}
description={t`Agents used to route queries to specialized agents`}
/>
<SettingsAIAgentsTable agents={data?.findManyAgents || []} />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,244 @@
import { ApolloError } from '@apollo/client';
import styled from '@emotion/styled';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { H2Title, IconTrash } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
CreateAgentInput,
useCreateOneAgentMutation,
useFindOneAgentQuery,
useUpdateOneAgentMutation,
} from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsAgentDeleteConfirmationModal } from './components/SettingsAgentDeleteConfirmationModal';
import { SettingsAgentDetailSkeletonLoader } from './components/SettingsAgentDetailSkeletonLoader';
import { SettingsAgentHandoffSection } from './components/SettingsAgentHandoffSection';
import { SettingsAIAgentForm } from './forms/components/SettingsAIAgentForm';
import { useSettingsAgentFormState } from './hooks/useSettingsAgentFormState';
const StyledContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
width: 100%;
`;
const DELETE_AGENT_MODAL_ID = 'delete-agent-modal';
export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const { agentId = '' } = useParams<{ agentId: string }>();
const navigate = useNavigateSettings();
const navigateApp = useNavigateApp();
const { enqueueErrorSnackBar } = useSnackBar();
const { openModal } = useModal();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isEditMode = mode === 'edit';
const {
formValues,
isSubmitting,
handleFieldChange,
resetForm,
setIsSubmitting,
validateForm,
} = useSettingsAgentFormState(mode);
const { data, loading } = useFindOneAgentQuery({
variables: { id: agentId },
skip: !isEditMode || !agentId,
onCompleted: (data) => {
const agent = data?.findOneAgent;
if (isDefined(agent)) {
resetForm({
name: agent.name,
label: agent.label,
description: agent.description,
icon: agent.icon || 'IconRobot',
modelId: agent.modelId,
role: agent.roleId,
prompt: agent.prompt,
isCustom: agent.isCustom,
});
} else {
enqueueErrorSnackBar({
message: t`Agent not found`,
});
navigateApp(AppPath.NotFound);
}
},
onError: (error) => {
enqueueErrorSnackBar({
apolloError: error,
});
navigateApp(AppPath.NotFound);
},
});
const [createAgent] = useCreateOneAgentMutation();
const [updateAgent] = useUpdateOneAgentMutation();
const agent = data?.findOneAgent;
const isAskAIAgent = agent?.id === currentWorkspace?.defaultAgent?.id;
if (isEditMode && !loading && !agent) {
return null;
}
const canSave = validateForm() && !isSubmitting;
const handleSave = async () => {
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
if (!isEditMode) {
const input: CreateAgentInput = {
name: formValues.name,
label: formValues.label,
description: formValues.description,
icon: formValues.icon,
modelId: formValues.modelId,
roleId: formValues.role || undefined,
prompt: formValues.prompt,
};
await createAgent({
variables: { input },
});
navigate(SettingsPath.AI);
return;
}
if (!agent) {
return;
}
await updateAgent({
variables: {
input: {
id: agent.id,
name: formValues.name || '',
label: formValues.label,
description: formValues.description,
icon: formValues.icon,
modelId: formValues.modelId,
roleId: formValues.role || undefined,
prompt: formValues.prompt,
},
},
});
navigate(SettingsPath.AI);
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
});
} finally {
setIsSubmitting(false);
}
};
const title = isEditMode ? (loading ? t`Agent` : agent?.label) : t`New Agent`;
const pageTitle = isEditMode ? t`Edit Agent` : t`New Agent`;
const pageDescription = isEditMode
? t`Update agent information`
: t`Create a new AI agent`;
const breadcrumbText = isEditMode
? loading
? t`Agent`
: agent?.label
: t`New Agent`;
return (
<>
<SubMenuTopBarContainer
title={title}
actionButton={
!isEditMode || !isAskAIAgent ? (
<SaveAndCancelButtons
onSave={handleSave}
onCancel={() => navigate(SettingsPath.AI)}
isSaveDisabled={!canSave}
isLoading={isSubmitting}
isCancelDisabled={isSubmitting}
/>
) : undefined
}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: t`AI`, href: getSettingsPath(SettingsPath.AI) },
{ children: breadcrumbText },
]}
>
<SettingsPageContainer>
<Section>
{!isAskAIAgent && (
<H2Title title={pageTitle} description={pageDescription} />
)}
{isEditMode && loading ? (
<SettingsAgentDetailSkeletonLoader />
) : (
<StyledContentContainer>
{isAskAIAgent ? (
<SettingsAgentHandoffSection agentId={agent?.id} />
) : (
<>
<SettingsAIAgentForm
formValues={formValues}
onFieldChange={handleFieldChange}
/>
{isEditMode && agent && formValues.isCustom && (
<Section>
<H2Title
title={t`Danger zone`}
description={t`Delete this agent`}
/>
<Button
accent="danger"
variant="secondary"
title={t`Delete Agent`}
Icon={IconTrash}
onClick={() => openModal(DELETE_AGENT_MODAL_ID)}
/>
</Section>
)}
</>
)}
</StyledContentContainer>
)}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
{isEditMode && agent && (
<SettingsAgentDeleteConfirmationModal
agentId={agent.id}
agentName={agent.label}
/>
)}
</>
);
};
@@ -0,0 +1,64 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Tag } from 'twenty-ui/components';
import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui/display';
import { Agent } from '~/generated-metadata/graphql';
export type SettingsAIAgentTableRowProps = {
action: ReactNode;
agent: Agent;
link?: string;
};
export const StyledAIAgentTableRow = styled(TableRow)`
grid-template-columns: 1fr 120px 36px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
min-width: 0;
overflow: hidden;
`;
const StyledActionTableCell = styled(TableCell)`
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconContainer = styled.div`
flex-shrink: 0;
`;
export const SettingsAIAgentTableRow = ({
action,
agent,
link,
}: SettingsAIAgentTableRowProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(agent.icon || 'IconRobot');
return (
<StyledAIAgentTableRow key={agent.id} to={link}>
<StyledNameTableCell>
<StyledIconContainer>
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
<OverflowingTextWithTooltip text={agent.label} />
</StyledNameTableCell>
<TableCell>
<Tag
text={agent.isCustom ? 'Custom' : 'Standard'}
color={agent.isCustom ? 'orange' : 'blue'}
weight="medium"
/>
</TableCell>
<StyledActionTableCell>{action}</StyledActionTableCell>
</StyledAIAgentTableRow>
);
};
@@ -0,0 +1,91 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { useTheme } from '@emotion/react';
import { IconChevronRight, IconSearch } from 'twenty-ui/display';
import { Agent } from '~/generated-metadata/graphql';
import { SETTINGS_AI_AGENT_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsAIAgentTableMetadata';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import {
SettingsAIAgentTableRow,
StyledAIAgentTableRow,
} from './SettingsAIAgentTableRow';
const StyledSearchInput = styled(TextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledTableHeaderRow = styled(StyledAIAgentTableRow)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAIAgentsTable = ({ agents }: { agents: Agent[] }) => {
const { t } = useLingui();
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const sortedAgents = useSortedArray(agents, SETTINGS_AI_AGENT_TABLE_METADATA);
const filteredAgents = sortedAgents.filter(
(agent) =>
agent.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
agent.label.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<>
<StyledSearchInput
instanceId="settings-ai-agents-search"
LeftIcon={IconSearch}
placeholder={t`Search an agent...`}
value={searchTerm}
onChange={setSearchTerm}
/>
<StyledTable>
<StyledTableHeaderRow>
{SETTINGS_AI_AGENT_TABLE_METADATA.fields.map(
(settingsAIAgentTableMetadataField) => (
<SortableTableHeader
key={settingsAIAgentTableMetadataField.fieldName}
fieldName={settingsAIAgentTableMetadataField.fieldName}
label={t(settingsAIAgentTableMetadataField.fieldLabel)}
tableId={SETTINGS_AI_AGENT_TABLE_METADATA.tableId}
align={settingsAIAgentTableMetadataField.align}
initialSort={SETTINGS_AI_AGENT_TABLE_METADATA.initialSort}
/>
),
)}
<TableHeader />
</StyledTableHeaderRow>
{filteredAgents.map((agent) => (
<SettingsAIAgentTableRow
key={agent.id}
agent={agent}
action={
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
link={getSettingsPath(SettingsPath.AIAgentDetail, {
agentId: agent.id,
})}
/>
))}
</StyledTable>
</>
);
};
@@ -0,0 +1,62 @@
import { ApolloError } from '@apollo/client';
import { Trans, useLingui } from '@lingui/react/macro';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SettingsPath } from '@/types/SettingsPath';
import { useDeleteOneAgentMutation } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const DELETE_AGENT_MODAL_ID = 'delete-agent-modal';
type SettingsAgentDeleteConfirmationModalProps = {
agentId: string;
agentName: string;
};
export const SettingsAgentDeleteConfirmationModal = ({
agentId,
agentName,
}: SettingsAgentDeleteConfirmationModalProps) => {
const { t } = useLingui();
const { closeModal } = useModal();
const navigate = useNavigateSettings();
const { enqueueErrorSnackBar } = useSnackBar();
const [deleteAgent] = useDeleteOneAgentMutation();
const handleDelete = async () => {
try {
await deleteAgent({
variables: {
input: { id: agentId },
},
});
closeModal(DELETE_AGENT_MODAL_ID);
navigate(SettingsPath.AI);
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
});
}
};
return (
<ConfirmationModal
confirmationValue={agentName}
confirmationPlaceholder={agentName}
modalId={DELETE_AGENT_MODAL_ID}
title={t`Delete Agent`}
subtitle={
<Trans>
This action cannot be undone. This will permanently delete your agent.
<br />
Please type in the agent name to confirm.
</Trans>
}
onConfirmClick={handleDelete}
confirmButtonText={t`Delete Agent`}
/>
);
};
@@ -0,0 +1,84 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
const StyledSkeletonContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
`;
const StyledFormSection = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconNameRow = styled.div`
align-items: flex-start;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconContainer = styled.div`
flex-shrink: 0;
`;
const StyledNameContainer = styled.div`
flex: 1;
`;
export const SettingsAgentDetailSkeletonLoader = () => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<StyledSkeletonContainer>
<StyledFormSection>
<StyledIconNameRow>
<StyledIconContainer>
<Skeleton
width={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
/>
</StyledIconContainer>
<StyledNameContainer>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
</StyledNameContainer>
</StyledIconNameRow>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
width="100%"
/>
<Skeleton height={120} width="100%" />
</StyledFormSection>
</StyledSkeletonContainer>
</SkeletonTheme>
);
};
@@ -0,0 +1,151 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea';
import { IconPlus } from 'twenty-ui/display';
import { Button, SelectOption } from 'twenty-ui/input';
import { useCreateAgentHandoffMutation } from '~/generated-metadata/graphql';
const StyledAddHandoffForm = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)};
padding: ${({ theme }) => theme.spacing(3)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledFormActions = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: flex-end;
`;
const StyledAddButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-top: ${({ theme }) => theme.spacing(3)};
`;
type SettingsAgentHandoffFormProps = {
agentId: string;
availableAgentOptions: SelectOption[];
agentsLoading: boolean;
onHandoffAdded: () => void;
};
export const SettingsAgentHandoffForm = ({
agentId,
availableAgentOptions,
agentsLoading,
onHandoffAdded,
}: SettingsAgentHandoffFormProps) => {
const { t } = useLingui();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [isAddingHandoff, setIsAddingHandoff] = useState(false);
const [selectedTargetAgentId, setSelectedTargetAgentId] = useState('');
const [handoffDescription, setHandoffDescription] = useState('');
const [createAgentHandoff] = useCreateAgentHandoffMutation();
const noAvailableAgents = availableAgentOptions.length === 0;
const resetHandoffForm = () => {
setIsAddingHandoff(false);
setSelectedTargetAgentId('');
setHandoffDescription('');
};
const handleAddHandoff = async () => {
try {
await createAgentHandoff({
variables: {
input: {
fromAgentId: agentId,
toAgentId: selectedTargetAgentId,
description: handoffDescription,
},
},
});
onHandoffAdded();
resetHandoffForm();
enqueueSuccessSnackBar({
message: t`Handoff created successfully`,
});
} catch (error) {
enqueueErrorSnackBar({
message: t`Failed to create handoff`,
});
}
};
return (
<>
{isAddingHandoff ? (
<StyledAddHandoffForm>
<Select
fullWidth
dropdownId="handoff-target-select"
label={t`Target Agent`}
value={selectedTargetAgentId}
onChange={setSelectedTargetAgentId}
options={availableAgentOptions}
emptyOption={{
label: t`Select a target agent`,
value: '',
}}
/>
<TextArea
textAreaId="handoff-description-textarea"
label={t`Description (Optional)`}
placeholder={t`Describe when this handoff should be used`}
minRows={2}
value={handoffDescription}
onChange={setHandoffDescription}
/>
<StyledFormActions>
<Button
size="small"
variant="secondary"
title={t`Cancel`}
onClick={resetHandoffForm}
/>
<Button
accent="blue"
size="small"
title={t`Add Handoff`}
onClick={handleAddHandoff}
disabled={!selectedTargetAgentId}
/>
</StyledFormActions>
</StyledAddHandoffForm>
) : (
<StyledAddButtonContainer>
<Button
variant="secondary"
size="small"
title={
agentsLoading
? t`Loading...`
: noAvailableAgents
? t`No agents available for handoff`
: t`Add Handoff`
}
Icon={IconPlus}
onClick={() => setIsAddingHandoff(true)}
disabled={agentsLoading || noAvailableAgents}
/>
</StyledAddButtonContainer>
)}
</>
);
};
@@ -0,0 +1,64 @@
import { useLingui } from '@lingui/react/macro';
import { H2Title } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
useFindAgentHandoffsQuery,
useFindManyAgentsQuery,
} from '~/generated-metadata/graphql';
import { SettingsAgentHandoffForm } from './SettingsAgentHandoffForm';
import { SettingsAgentHandoffTable } from './SettingsAgentHandoffTable';
export const SettingsAgentHandoffSection = ({
agentId,
}: {
agentId: string;
}) => {
const { t } = useLingui();
const { data: agentsData, loading: agentsLoading } = useFindManyAgentsQuery();
const { data: handoffData, refetch: refetchHandoffTargets } =
useFindAgentHandoffsQuery({
variables: { input: { id: agentId } },
skip: !agentId,
});
const handoffTargets = handoffData?.findAgentHandoffs || [];
const availableAgentOptions =
agentsData?.findManyAgents?.reduce<SelectOption[]>((acc, agent) => {
if (
agent.id !== agentId &&
!handoffTargets.some((handoff: any) => handoff.toAgent.id === agent.id)
) {
acc.push({
label: agent.label,
value: agent.id,
});
}
return acc;
}, []) || [];
return (
<Section>
<H2Title
title={t`Agent Handoffs`}
description={t`Configure which agents this agent can hand off conversations to`}
/>
<SettingsAgentHandoffTable
agentId={agentId}
handoffTargets={handoffTargets}
onHandoffRemoved={refetchHandoffTargets}
/>
<SettingsAgentHandoffForm
agentId={agentId}
availableAgentOptions={availableAgentOptions}
agentsLoading={agentsLoading}
onHandoffAdded={refetchHandoffTargets}
/>
</Section>
);
};
@@ -0,0 +1,183 @@
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import {
IconSearch,
IconTrash,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { useRemoveAgentHandoffMutation } from '~/generated-metadata/graphql';
import { AgentHandoffDto } from '~/generated/graphql';
const AGENT_HANDOFF_DELETION_MODAL_ID = 'agent-handoff-deletion-modal';
const StyledButtonContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
margin-left: ${({ theme }) => theme.spacing(3)};
`;
const StyledTable = styled(Table)`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledSearchContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTableRows = styled.div`
padding-block: ${({ theme }) => theme.spacing(2)};
`;
const StyledNoHandoffs = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledTableCell = styled(TableCell)`
overflow: hidden;
`;
type SettingsAgentHandoffTableProps = {
agentId: string;
handoffTargets: AgentHandoffDto[];
onHandoffRemoved: () => void;
};
export const SettingsAgentHandoffTable = ({
agentId,
handoffTargets,
onHandoffRemoved,
}: SettingsAgentHandoffTableProps) => {
const { t } = useLingui();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [searchFilter, setSearchFilter] = useState('');
const [handoffToDelete, setHandoffToDelete] = useState<string | undefined>();
const [removeAgentHandoff] = useRemoveAgentHandoffMutation();
const filteredHandoffTargets = !searchFilter
? handoffTargets
: handoffTargets.filter((handoff) => {
const searchTerm = searchFilter.toLowerCase();
const label = handoff.toAgent.label?.toLowerCase() || '';
const description = handoff.description?.toLowerCase() || '';
return label.includes(searchTerm) || description.includes(searchTerm);
});
const handleRemoveHandoff = async () => {
if (!handoffToDelete) {
return;
}
try {
await removeAgentHandoff({
variables: {
input: {
fromAgentId: agentId,
toAgentId: handoffToDelete,
},
},
});
onHandoffRemoved();
enqueueSuccessSnackBar({
message: t`Handoff removed successfully`,
});
} catch (error) {
enqueueErrorSnackBar({
message: t`Failed to remove handoff`,
});
}
};
const { openModal } = useModal();
return (
<>
<StyledSearchContainer>
<TextInputV2
value={searchFilter}
onChange={setSearchFilter}
placeholder={t`Search handoffs...`}
fullWidth
LeftIcon={IconSearch}
sizeVariant="lg"
/>
</StyledSearchContainer>
<StyledTable>
<TableRow gridAutoColumns="2fr 2fr 1fr">
<TableHeader>
<Trans>Target Agent</Trans>
</TableHeader>
<TableHeader>
<Trans>Description</Trans>
</TableHeader>
<TableHeader align={'right'} />
</TableRow>
<StyledTableRows>
{filteredHandoffTargets.length > 0 ? (
filteredHandoffTargets.map((handoff) => (
<TableRow gridAutoColumns="2fr 2fr 1fr" key={handoff.id}>
<TableCell>
<OverflowingTextWithTooltip text={handoff.toAgent.label} />
</TableCell>
<StyledTableCell>
<OverflowingTextWithTooltip
text={handoff.description || t`No description`}
/>
</StyledTableCell>
<TableCell align={'right'}>
<StyledButtonContainer>
<IconButton
onClick={() => {
openModal(AGENT_HANDOFF_DELETION_MODAL_ID);
setHandoffToDelete(handoff.toAgent.id);
}}
variant="tertiary"
size="medium"
Icon={IconTrash}
/>
</StyledButtonContainer>
</TableCell>
</TableRow>
))
) : (
<StyledNoHandoffs>
{!searchFilter
? t`No handoffs configured for this agent`
: t`No handoffs match your search`}
</StyledNoHandoffs>
)}
</StyledTableRows>
</StyledTable>
<ConfirmationModal
modalId={AGENT_HANDOFF_DELETION_MODAL_ID}
title={t`Remove Handoff`}
subtitle={
<Trans>
This action cannot be undone. This will permanently remove the
handoff configuration.
</Trans>
}
onConfirmClick={handleRemoveHandoff}
confirmButtonText={t`Remove handoff`}
/>
</>
);
};
@@ -0,0 +1,25 @@
import { TableMetadata } from '@/ui/layout/table/types/TableMetadata';
import { msg } from '@lingui/core/macro';
import { Agent } from '~/generated-metadata/graphql';
export const SETTINGS_AI_AGENT_TABLE_METADATA: TableMetadata<Agent> = {
tableId: 'settingsAIAgent',
fields: [
{
fieldLabel: msg`Name`,
fieldName: 'name',
fieldType: 'string',
align: 'left',
},
{
fieldLabel: msg`Type`,
fieldName: 'isCustom',
fieldType: 'string',
align: 'left',
},
],
initialSort: {
fieldName: 'name',
orderBy: 'AscNullsLast',
},
};
@@ -0,0 +1,144 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput';
import { isDefined } from 'twenty-shared/utils';
import { useGetRolesQuery } from '~/generated-metadata/graphql';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import { SettingsAIAgentFormValues } from '../../hooks/useSettingsAgentFormState';
const StyledFormContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconNameRow = styled.div`
align-items: flex-start;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledNameContainer = styled.div`
flex: 1;
`;
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
font-size: ${({ theme }) => theme.font.size.sm};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
type SettingsAIAgentFormProps = {
formValues: SettingsAIAgentFormValues;
onFieldChange: (
field: keyof SettingsAIAgentFormValues,
value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues],
) => void;
};
export const SettingsAIAgentForm = ({
formValues,
onFieldChange,
}: SettingsAIAgentFormProps) => {
const { t } = useLingui();
const modelOptions = useAiModelOptions();
const { data: rolesData } = useGetRolesQuery();
const rolesOptions =
rolesData?.getRoles?.map((role) => ({
label: role.label,
value: role.id,
})) || [];
const noModelsAvailable = modelOptions.length === 0;
const fillNameFromLabel = (label: string) => {
isDefined(label) &&
onFieldChange('name', computeMetadataNameFromLabel(label));
};
return (
<StyledFormContainer>
<StyledFormContainer>
<StyledIconNameRow>
<IconPicker
selectedIconKey={formValues.icon || 'IconRobot'}
onChange={({ iconKey }) => {
onFieldChange('icon', iconKey);
}}
/>
<StyledNameContainer>
<TextInput
instanceId="agent-label-input"
placeholder={t`Enter agent name*`}
value={formValues.label}
onChange={(value) => {
onFieldChange('label', value);
fillNameFromLabel(value);
}}
fullWidth
/>
</StyledNameContainer>
</StyledIconNameRow>
</StyledFormContainer>
<StyledFormContainer>
<TextArea
textAreaId="agent-description-textarea"
placeholder={t`Write a description for this agent`}
minRows={3}
value={formValues.description || ''}
onChange={(value) => onFieldChange('description', value)}
/>
</StyledFormContainer>
<StyledFormContainer>
<Select
dropdownId="ai-model-select"
label={t`AI Model`}
value={formValues.modelId}
onChange={(value) => onFieldChange('modelId', value)}
options={modelOptions}
disabled={noModelsAvailable}
/>
{noModelsAvailable && (
<StyledErrorMessage>
{t`No models available. Please configure AI models in your workspace settings.`}
</StyledErrorMessage>
)}
</StyledFormContainer>
<StyledFormContainer>
<Select
dropdownId="ai-role-select"
label={t`Role`}
value={formValues.role || ''}
onChange={(value) => onFieldChange('role', value)}
options={rolesOptions}
emptyOption={{
label: t`Select a role`,
value: '',
}}
/>
</StyledFormContainer>
<StyledFormContainer>
<TextArea
textAreaId="agent-prompt-textarea"
label={t`System Prompt*`}
placeholder={t`Enter the system prompt that defines this agent's behavior and capabilities`}
minRows={6}
value={formValues.prompt}
onChange={(value) => onFieldChange('prompt', value)}
/>
</StyledFormContainer>
</StyledFormContainer>
);
};
@@ -0,0 +1,65 @@
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { settingsAIAgentFormSchema } from '../validation-schemas/settingsAIAgentFormSchema';
export type SettingsAIAgentFormValues = z.infer<
typeof settingsAIAgentFormSchema
>;
export const useSettingsAgentFormState = (mode: 'create' | 'edit') => {
const [formValues, setFormValues] = useState<SettingsAIAgentFormValues>({
name: '',
label: '',
description: '',
icon: 'IconRobot',
modelId: mode === 'edit' ? '' : 'auto',
role: '',
prompt: '',
isCustom: true,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateForm = (): boolean => {
try {
settingsAIAgentFormSchema.parse(formValues);
return true;
} catch (error) {
return false;
}
};
const handleFieldChange = (
field: keyof SettingsAIAgentFormValues,
value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues],
) => {
setFormValues((prev) => ({ ...prev, [field]: value }));
};
const resetForm = (values?: Partial<SettingsAIAgentFormValues>) => {
if (isDefined(values)) {
setFormValues((prev) => ({ ...prev, ...values }));
} else {
setFormValues({
name: '',
label: '',
description: '',
icon: 'IconRobot',
modelId: mode === 'edit' ? '' : 'auto',
role: '',
prompt: '',
isCustom: true,
});
}
};
return {
formValues,
isSubmitting,
handleFieldChange,
resetForm,
setIsSubmitting,
validateForm,
};
};
@@ -0,0 +1,17 @@
import { z } from 'zod';
import { zodNonEmptyString } from '~/types/ZodNonEmptyString';
export const settingsAIAgentFormSchema = z.object({
name: z.string().optional(),
label: zodNonEmptyString,
description: z.string().nullish(),
icon: z.string().optional(),
modelId: z.union([z.string().min(1, 'Model is required'), z.literal('auto')]),
role: z.string().optional(),
prompt: zodNonEmptyString,
isCustom: z.boolean().default(true),
});
export type SettingsAIAgentFormValues = z.infer<
typeof settingsAIAgentFormSchema
>;
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -14,7 +13,6 @@ import { AgentRoleService } from './agent-role.service';
[AgentEntity, RoleEntity, RoleTargetsEntity],
'core',
),
AgentModule,
],
providers: [AgentRoleService],
exports: [AgentRoleService],
@@ -17,6 +17,7 @@ import { In, Repository } from 'typeorm';
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils';
@@ -34,9 +35,7 @@ import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/util
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { AgentEntity } from './agent.entity';
import { AgentException, AgentExceptionCode } from './agent.exception';
@@ -96,20 +95,22 @@ export class AgentExecutionService {
agent,
}: {
system: string;
agent: AgentEntity;
agent: AgentEntity | null;
prompt?: string;
messages?: CoreMessage[];
}) {
try {
this.logger.log(
`Preparing AI request config for agent ${agent.id} with model ${agent.modelId}`,
);
if (agent) {
this.logger.log(
`Preparing AI request config for agent ${agent.id} with model ${agent.modelId}`,
);
}
const aiModel = this.aiModelRegistryService.getEffectiveModelConfig(
agent.modelId,
agent?.modelId ?? 'auto',
);
if (!aiModel) {
if (agent && !aiModel) {
const error = `AI model with id ${agent.modelId} not found`;
this.logger.error(error);
@@ -127,10 +128,12 @@ export class AgentExecutionService {
await this.validateApiKey(provider);
const tools = await this.agentToolService.generateToolsForAgent(
agent.id,
agent.workspaceId,
);
const tools = agent
? await this.agentToolService.generateToolsForAgent(
agent.id,
agent.workspaceId,
)
: {};
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
@@ -155,7 +158,7 @@ export class AgentExecutionService {
};
} catch (error) {
this.logger.error(
`Failed to prepare AI request config for agent ${agent.id}:`,
`Failed to prepare AI request config for agent ${agent?.id ?? 'no agent'}`,
error instanceof Error ? error.stack : error,
);
throw error;
@@ -343,18 +346,19 @@ export class AgentExecutionService {
async executeAgent({
agent,
context,
schema,
userPrompt,
}: {
agent: AgentEntity;
agent: AgentEntity | null;
context: Record<string, unknown>;
schema: OutputSchema;
userPrompt: string;
}): Promise<AgentExecutionResult> {
try {
const aiRequestConfig = await this.prepareAIRequestConfig({
system: AGENT_SYSTEM_PROMPTS.AGENT_EXECUTION,
system: `You are executing as part of a workflow automation. ${agent ? agent.prompt : ''}`,
agent,
prompt: resolveInput(agent.prompt, context) as string,
prompt: userPrompt,
});
const textResponse = await generateText(aiRequestConfig);
@@ -5,6 +5,7 @@ import { generateText } from 'ai';
import { Repository } from 'typeorm';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AGENT_HANDOFF_PROMPT_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-prompt.const';
import { AgentHandoffService } from './agent-handoff.service';
import { AgentEntity } from './agent.entity';
@@ -31,7 +32,7 @@ export class AgentHandoffExecutorService {
async executeHandoff(handoffRequest: HandoffRequest) {
try {
const { fromAgentId, toAgentId, workspaceId, reason } = handoffRequest;
const { fromAgentId, toAgentId, workspaceId } = handoffRequest;
const canHandoff = await this.agentHandoffService.canHandoffTo({
fromAgentId,
@@ -76,13 +77,7 @@ export class AgentHandoffExecutorService {
const textResponse = await generateText(aiRequestConfig);
return {
success: true,
newAgentId: toAgentId,
newAgentName: targetAgent.name,
message: `Successfully transferred to ${targetAgent.name}. ${reason}`,
response: textResponse.text,
};
return textResponse.text;
} catch (error) {
this.logger.error(
`Handoff execution failed: ${error.message}`,
@@ -101,17 +96,9 @@ export class AgentHandoffExecutorService {
private createHandoffPrompt(handoffRequest: HandoffRequest): string {
const { reason, context } = handoffRequest;
const prompt = `
You have received a handoff from another AI agent. This means the previous agent has determined that you are better suited to handle this conversation based on your specialized knowledge and capabilities.
The previous agent has transferred this conversation to you because: ${reason}
Additional context from the previous agent:
${context || 'No additional context provided'}
Please continue the conversation naturally, acknowledging that you are taking over from the previous agent. Use your specialized knowledge to provide the best possible assistance to the user.
`;
return prompt;
return AGENT_HANDOFF_PROMPT_TEMPLATE.replace('{reason}', reason).replace(
'{context}',
context || 'No additional context provided',
);
}
}
@@ -54,6 +54,24 @@ export class AgentHandoffService {
return handoffs.map((handoff) => handoff.toAgent);
}
async getAgentHandoffs({
fromAgentId,
workspaceId,
}: {
fromAgentId: string;
workspaceId: string;
}): Promise<AgentHandoffEntity[]> {
const handoffs = await this.agentHandoffRepository.find({
where: {
fromAgentId,
workspaceId,
},
relations: ['toAgent'],
});
return handoffs;
}
async createHandoff({
fromAgentId,
toAgentId,
@@ -10,6 +10,7 @@ import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { AGENT_HANDOFF_DESCRIPTION_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-description.const';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { camelCase } from 'src/utils/camel-case';
@@ -67,49 +68,59 @@ export class AgentToolService {
agentId: string,
workspaceId: string,
): Promise<ToolSet> {
const handoffTargets = await this.agentHandoffService.getHandoffTargets({
const handoffs = await this.agentHandoffService.getAgentHandoffs({
fromAgentId: agentId,
workspaceId,
});
const handoffTools = handoffTargets.reduce<ToolSet>(
(tools, targetAgent) => {
const toolName = `transfer_to_${camelCase(targetAgent.name)}`;
const handoffTools = handoffs.reduce<ToolSet>((tools, handoff) => {
const toolName = `handoff_to_${camelCase(handoff.toAgent.name)}`;
const handoffSchema = z.object({
reason: z.string().describe('Reason for transferring to this agent'),
const handoffSchema = z.object({
toolDescription: z
.string()
.describe(
"A clear, human-readable status message describing the handoff being made. This will be shown to the user while the handoff is being processed, so phrase it as a present-tense status update (e.g., 'Transferring you to the sales agent for pricing information').",
),
input: z.object({
reason: z
.string()
.describe(
'Brief explanation of why this handoff is needed (e.g., "User needs pricing information", "User requires technical support", "User wants to discuss billing")',
),
context: z
.string()
.optional()
.describe('Additional context to pass to the receiving agent'),
});
.describe(
'Any relevant context or information to pass to the receiving agent (e.g., user preferences, previous conversation details, specific requirements)',
),
}),
});
tools[toolName] = {
description: `Transfer this request to ${targetAgent.name} when you need their specialized expertise. Use this when the user's request is outside your capabilities or when ${targetAgent.name} would be better suited to handle the request.`,
parameters: handoffSchema,
execute: async ({ reason, context }) => {
const result =
await this.agentHandoffExecutorService.executeHandoff({
fromAgentId: agentId,
toAgentId: targetAgent.id,
workspaceId,
reason,
context,
});
tools[toolName] = {
description:
handoff.description ||
handoff.toAgent.description ||
AGENT_HANDOFF_DESCRIPTION_TEMPLATE.replace(
'{agentName}',
handoff.toAgent.name,
),
parameters: handoffSchema,
execute: async ({ input: { reason, context } }) => {
const result = await this.agentHandoffExecutorService.executeHandoff({
fromAgentId: agentId,
toAgentId: handoff.toAgent.id,
workspaceId,
reason,
context,
});
return {
success: result.success,
message: result.message || `Transferred to ${targetAgent.name}`,
newAgentId: result.newAgentId,
newAgentName: result.newAgentName,
};
},
};
return result;
},
};
return tools;
},
{},
);
return tools;
}, {});
return handoffTools;
}
@@ -4,19 +4,20 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
import { AgentChatController } from 'src/engine/metadata-modules/agent/agent-chat.controller';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { AgentChatMessageEntity } from './agent-chat-message.entity';
import { AgentChatThreadEntity } from './agent-chat-thread.entity';
@@ -49,6 +50,7 @@ import { AgentService } from './agent.service';
'core',
),
AiModule,
AgentRoleModule,
ThrottlerModule,
AuditModule,
FeatureFlagModule,
@@ -1,5 +1,8 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -9,17 +12,37 @@ import {
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateAgentHandoffInput } from 'src/engine/metadata-modules/agent/dtos/create-agent-handoff.input';
import { RemoveAgentHandoffInput } from 'src/engine/metadata-modules/agent/dtos/remove-agent-handoff.input';
import { AgentHandoffService } from './agent-handoff.service';
import { AgentEntity } from './agent.entity';
import { AgentService } from './agent.service';
import { AgentHandoffDTO } from './dtos/agent-handoff.dto';
import { AgentIdInput } from './dtos/agent-id.input';
import { AgentDTO } from './dtos/agent.dto';
import { CreateAgentInput } from './dtos/create-agent.input';
import { UpdateAgentInput } from './dtos/update-agent.input';
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
@Resolver()
export class AgentResolver {
constructor(private readonly agentService: AgentService) {}
constructor(
@InjectRepository(AgentEntity, 'core')
private readonly agentRepository: Repository<AgentEntity>,
private readonly agentService: AgentService,
private readonly agentHandoffService: AgentHandoffService,
) {}
@Query(() => [AgentDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async findManyAgents(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.agentRepository.find({
where: { workspaceId },
order: { createdAt: 'DESC' },
});
}
@Query(() => AgentDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
@@ -30,6 +53,42 @@ export class AgentResolver {
return this.agentService.findOneAgent(id, workspaceId);
}
@Query(() => [AgentDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async findAgentHandoffTargets(
@Args('input') { id }: AgentIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.agentHandoffService.getHandoffTargets({
fromAgentId: id,
workspaceId,
});
}
@Query(() => [AgentHandoffDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async findAgentHandoffs(
@Args('input') { id }: AgentIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.agentHandoffService.getAgentHandoffs({
fromAgentId: id,
workspaceId,
});
}
@Mutation(() => AgentDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async createOneAgent(
@Args('input') input: CreateAgentInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.agentService.createOneAgent(
{ ...input, isCustom: true },
workspaceId,
);
}
@Mutation(() => AgentDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async updateOneAgent(
@@ -38,4 +97,44 @@ export class AgentResolver {
) {
return this.agentService.updateOneAgent(input, workspaceId);
}
@Mutation(() => AgentDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async deleteOneAgent(
@Args('input') { id }: AgentIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.agentService.deleteOneAgent(id, workspaceId);
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async createAgentHandoff(
@Args('input') input: CreateAgentHandoffInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
await this.agentHandoffService.createHandoff({
fromAgentId: input.fromAgentId,
toAgentId: input.toAgentId,
workspaceId,
description: input.description,
});
return true;
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async removeAgentHandoff(
@Args('input') input: RemoveAgentHandoffInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
await this.agentHandoffService.removeHandoff({
fromAgentId: input.fromAgentId,
toAgentId: input.toAgentId,
workspaceId,
});
return true;
}
}
@@ -1,11 +1,15 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
import { CreateAgentInput } from 'src/engine/metadata-modules/agent/dtos/create-agent.input';
import { UpdateAgentInput } from 'src/engine/metadata-modules/agent/dtos/update-agent.input';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label.util';
import { AgentEntity } from './agent.entity';
import { AgentException, AgentExceptionCode } from './agent.exception';
@@ -18,15 +22,9 @@ export class AgentService {
@InjectRepository(RoleTargetsEntity, 'core')
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly agentChatService: AgentChatService,
private readonly agentRoleService: AgentRoleService,
) {}
async findManyAgents(workspaceId: string) {
return this.agentRepository.find({
where: { workspaceId },
order: { createdAt: 'DESC' },
});
}
async findOneAgent(id: string, workspaceId: string) {
const agent = await this.agentRepository.findOne({
where: { id, workspaceId },
@@ -53,71 +51,63 @@ export class AgentService {
};
}
async createOneAgentAndFirstThread(
input: {
name: string;
label: string;
description?: string;
prompt: string;
modelId: ModelId;
},
workspaceId: string,
userWorkspaceId: string | null,
) {
const agent = await this.createOneAgent(input, workspaceId);
if (!userWorkspaceId) {
throw new AgentException(
'User workspace ID not found',
AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND,
);
}
await this.agentChatService.createThread(agent.id, userWorkspaceId);
return agent;
}
async createOneAgent(
input: {
name: string;
label: string;
description?: string;
prompt: string;
modelId: ModelId;
responseFormat?: object;
},
input: CreateAgentInput & { isCustom: boolean },
workspaceId: string,
) {
const agent = this.agentRepository.create({
...input,
name: input.name || computeMetadataNameFromLabel(input.label),
workspaceId,
isCustom: input.isCustom,
});
const createdAgent = await this.agentRepository.save(agent);
if (input.roleId) {
await this.agentRoleService.assignRoleToAgent({
workspaceId,
agentId: createdAgent.id,
roleId: input.roleId,
});
}
return this.findOneAgent(createdAgent.id, workspaceId);
}
async updateOneAgent(
input: {
id: string;
name?: string;
description?: string;
prompt?: string;
modelId?: ModelId;
responseFormat?: object;
},
workspaceId: string,
) {
async updateOneAgent(input: UpdateAgentInput, workspaceId: string) {
const agent = await this.findOneAgent(input.id, workspaceId);
let updatedName = input.name;
if (input.label) {
updatedName = computeMetadataNameFromLabel(input.label);
}
const updatedAgent = await this.agentRepository.save({
...agent,
...input,
name: updatedName,
});
return updatedAgent;
if (!isDefined(input.roleId)) {
return updatedAgent;
}
if (input.roleId) {
await this.agentRoleService.assignRoleToAgent({
workspaceId,
agentId: agent.id,
roleId: input.roleId,
});
} else {
await this.agentRoleService.removeRoleFromAgent({
workspaceId,
agentId: agent.id,
});
}
return this.findOneAgent(updatedAgent.id, workspaceId);
}
async deleteOneAgent(id: string, workspaceId: string) {
@@ -0,0 +1,2 @@
export const AGENT_HANDOFF_DESCRIPTION_TEMPLATE =
"Use this tool when the user's request requires {agentName}'s specialized expertise or capabilities. CRITICAL: You MUST call this tool function immediately. Do NOT respond with text about transferring - execute the tool instead. This is a FUNCTION CALL - you must invoke it, not describe it.";
@@ -0,0 +1,11 @@
export const AGENT_HANDOFF_PROMPT_TEMPLATE = `You have received a handoff from another AI agent who determined that you are better suited to handle this conversation based on your specialized knowledge and capabilities.
**Reason for handoff:** {reason}
**Context from the previous agent:**
{context}
**Instructions:**
- Continue the conversation naturally and professionally
- Leverage your specialized expertise to provide the best possible assistance
- Maintain context from the previous conversation while adding your unique value`;
@@ -41,20 +41,20 @@ Guidelines:
- Pay special attention to any data returned from tool executions (database queries, HTTP requests, record creation, etc.)`,
AGENT_CHAT: `You are a helpful AI assistant for this workspace. You can:
- Answer questions conversationally, clearly, and helpfully
- Provide insights, support, and updates about people, companies, opportunities, tasks, notes, and other business objects.
- Answer questions about people, companies, opportunities, tasks, notes, and other business objects
- Access and summarize information you have permission to see
- Help users understand how to use the system and its features
- Use various tools that are provided to you dynamically when needed
- Use tools provided to you dynamically when needed
- Transfer conversations to other specialized agents when their expertise is better suited
Permissions and capabilities:
- You can only perform actions and access data that your assigned role and permissions allow
- If a user requests something you do not have permission for, politely explain the limitation (e.g., "I cannot perform this operation because I don't have the necessary permissions. Please check your role or contact an admin.")
- If you are unsure about your permissions for a specific action, ask the user for clarification or suggest they check with an administrator
- Do not attempt to simulate or fake actions you cannot perform
- Only use tools that are actually available to you through the tools property
Permissions:
- Only perform actions and access data that your assigned role and permissions allow
- If you lack permissions, politely explain the limitation
- Only use tools that are actually available to you
If you need more information to answer a question, ask follow-up questions. Always be transparent about your capabilities and limitations.
Agent handoff:
- Use handoff tools when the user's request requires expertise outside your capabilities
- IMPORTANT: Do not respond with text about transferring - execute the handoff tool function
- Use the response returned by the handoff agent as your reply to the user
When formatting responses:
- Use markdown syntax to improve readability of long responses
@@ -63,5 +63,5 @@ When formatting responses:
- Create tables when presenting structured data
- Use blockquotes for important notes or callouts
Note: This base system prompt will be combined with the agent's specific instructions and context to provide you with complete guidance for your role.`,
Note: This base system prompt will be combined with the agent's specific instructions and context.`,
};
@@ -0,0 +1,17 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AgentDTO } from './agent.dto';
@ObjectType()
export class AgentHandoffDTO {
@Field(() => UUIDScalarType)
id: string;
@Field({ nullable: true })
description?: string;
@Field(() => AgentDTO)
toAgent: AgentDTO;
}
@@ -1,6 +1,12 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { IsDateString, IsNotEmpty, IsString, IsUUID } from 'class-validator';
import {
IsBoolean,
IsDateString,
IsNotEmpty,
IsString,
IsUUID,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -17,6 +23,14 @@ export class AgentDTO {
@Field()
name: string;
@IsString()
@Field()
label: string;
@IsString()
@Field({ nullable: true })
icon?: string;
@IsString()
@Field({ nullable: true })
description: string;
@@ -35,6 +49,10 @@ export class AgentDTO {
@Field(() => UUIDScalarType, { nullable: true })
roleId?: string;
@IsBoolean()
@Field()
isCustom: boolean;
@HideField()
workspaceId: string;
@@ -0,0 +1,15 @@
import { Field, InputType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateAgentHandoffInput {
@Field(() => UUIDScalarType)
fromAgentId: string;
@Field(() => UUIDScalarType)
toAgentId: string;
@Field({ nullable: true })
description?: string;
}
@@ -1,16 +1,33 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
import {
IsNotEmpty,
IsObject,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
@InputType()
export class CreateAgentInput {
@IsString()
@IsOptional()
@Field({ nullable: true })
name?: string;
@IsString()
@IsNotEmpty()
@Field()
name: string;
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsString()
@IsOptional()
@@ -27,6 +44,11 @@ export class CreateAgentInput {
@Field(() => String)
modelId: ModelId;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
roleId?: string;
@IsObject()
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
@@ -0,0 +1,10 @@
import { Field, InputType } from '@nestjs/graphql';
@InputType()
export class RemoveAgentHandoffInput {
@Field()
fromAgentId: string;
@Field()
toAgentId: string;
}
@@ -24,6 +24,16 @@ export class UpdateAgentInput {
@Field()
name?: string;
@IsString()
@IsOptional()
@Field()
label?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
@@ -39,6 +49,11 @@ export class UpdateAgentInput {
@Field(() => String)
modelId?: ModelId;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
roleId?: string;
@IsObject()
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
@@ -217,6 +217,7 @@ export class WorkspaceManagerService {
description: 'Default Routing Agent',
prompt: '',
modelId: 'auto',
isCustom: false,
},
workspaceId,
);
@@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { t } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { StepStatus } from 'twenty-shared/workflow';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { StepStatus } from 'twenty-shared/workflow';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
@@ -403,12 +403,16 @@ export class WorkflowVersionStepWorkspaceService {
break;
}
case WorkflowActionType.AI_AGENT: {
if (!isDefined(step.settings.input.agentId)) {
break;
}
const agent = await this.agentService.findOneAgent(
step.settings.input.agentId,
workspaceId,
);
if (agent) {
if (isDefined(agent)) {
await this.agentService.deleteOneAgent(agent.id, workspaceId);
}
break;
@@ -611,25 +615,6 @@ export class WorkflowVersionStepWorkspaceService {
};
}
case WorkflowActionType.AI_AGENT: {
const newAgent = await this.agentService.createOneAgentAndFirstThread(
{
label: 'AI Agent Workflow Step',
name: `ai-agent-workflow-${newStepId}`,
description: 'Created automatically for workflow step',
prompt: '',
modelId: 'auto',
},
workspaceId,
this.scopedWorkspaceContextFactory.create().userWorkspaceId,
);
if (!isDefined(newAgent)) {
throw new WorkflowVersionStepException(
'Failed to create AI Agent Step',
WorkflowVersionStepExceptionCode.FAILURE,
);
}
return {
id: newStepId,
name: 'AI Agent',
@@ -638,7 +623,8 @@ export class WorkflowVersionStepWorkspaceService {
settings: {
...BASE_STEP_DEFINITION,
input: {
agentId: newAgent.id,
agentId: '',
prompt: '',
},
},
};
@@ -18,6 +18,7 @@ import {
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
import { isWorkflowAiAgentAction } from './guards/is-workflow-ai-agent-action.guard';
@@ -51,18 +52,22 @@ export class AiAgentWorkflowAction implements WorkflowAction {
);
}
const { agentId } = step.settings.input;
const { agentId, prompt } = step.settings.input;
const workspaceId = context.workspaceId as string;
try {
const agent = await this.agentRepository.findOne({
where: {
id: agentId,
workspaceId,
},
});
let agent: AgentEntity | null = null;
if (!agent) {
if (agentId) {
agent = await this.agentRepository.findOne({
where: {
id: agentId,
workspaceId,
},
});
}
if (agentId && !agent) {
throw new AgentException(
`Agent with id ${agentId} not found`,
AgentExceptionCode.AGENT_NOT_FOUND,
@@ -73,10 +78,11 @@ export class AiAgentWorkflowAction implements WorkflowAction {
agent,
context,
schema: step.settings.outputSchema,
userPrompt: resolveInput(prompt, context) as string,
});
await this.aiBillingService.calculateAndBillUsage(
agent.modelId,
agent?.modelId ?? 'auto',
usage,
workspaceId,
);
@@ -1,3 +1,4 @@
export type WorkflowAiAgentActionInput = {
agentId: string;
agentId?: string;
prompt?: string;
};
@@ -1,11 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
import { AgentHandoffEntity } from 'src/engine/metadata-modules/agent/agent-handoff.entity';
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/agent/agent.exception';
import { AgentResolver } from 'src/engine/metadata-modules/agent/agent.resolver';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
// Mock the agent service
jest.mock('../../../../../src/engine/metadata-modules/agent/agent.service');
@@ -41,6 +48,52 @@ describe('agentResolver', () => {
updateOneAgent: jest.fn(),
},
},
{
provide: AgentHandoffService,
useValue: {
getHandoffTargets: jest.fn(),
canHandoffTo: jest.fn(),
createHandoff: jest.fn(),
removeHandoff: jest.fn(),
},
},
{
provide: getRepositoryToken(AgentEntity, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
},
},
{
provide: getRepositoryToken(AgentHandoffEntity, 'core'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
},
},
{
provide: getRepositoryToken(RoleTargetsEntity, 'core'),
useValue: {
findOne: jest.fn(),
},
},
{
provide: AgentChatService,
useValue: {
createThread: jest.fn(),
},
},
{
provide: AgentRoleService,
useValue: {
assignRoleToAgent: jest.fn(),
removeRoleFromAgent: jest.fn(),
},
},
],
}).compile();
@@ -119,6 +119,7 @@ export const createAgentToolTestModule =
useValue: {
getHandoffTargets: jest.fn().mockResolvedValue([]),
canHandoffTo: jest.fn().mockResolvedValue(true),
getAgentHandoffs: jest.fn().mockResolvedValue([]),
},
},
{