diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index fdd9b154390..b18ec539124 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -4157,11 +4157,11 @@ export type UpdateAgentInput = { description?: InputMaybe; icon?: InputMaybe; id: Scalars['UUID']; - label: Scalars['String']; + label?: InputMaybe; modelConfiguration?: InputMaybe; - modelId: Scalars['String']; - name: Scalars['String']; - prompt: Scalars['String']; + modelId?: InputMaybe; + name?: InputMaybe; + prompt?: InputMaybe; responseFormat?: InputMaybe; roleId?: InputMaybe; }; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 3b8729b82a3..15f5ac894c7 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -4027,11 +4027,11 @@ export type UpdateAgentInput = { description?: InputMaybe; icon?: InputMaybe; id: Scalars['UUID']; - label: Scalars['String']; + label?: InputMaybe; modelConfiguration?: InputMaybe; - modelId: Scalars['String']; - name: Scalars['String']; - prompt: Scalars['String']; + modelId?: InputMaybe; + name?: InputMaybe; + prompt?: InputMaybe; responseFormat?: InputMaybe; roleId?: InputMaybe; }; diff --git a/packages/twenty-front/src/modules/ai/constants/OutputFieldTypeOptions.ts b/packages/twenty-front/src/modules/ai/constants/OutputFieldTypeOptions.ts index 4c6e3379da7..266a9f5db88 100644 --- a/packages/twenty-front/src/modules/ai/constants/OutputFieldTypeOptions.ts +++ b/packages/twenty-front/src/modules/ai/constants/OutputFieldTypeOptions.ts @@ -1,8 +1,6 @@ -import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; +import { type AgentResponseFieldType } from 'twenty-shared/ai'; import { msg } from '@lingui/core/macro'; -import { FieldMetadataType } from 'twenty-shared/types'; import { - IllustrationIconCalendarEvent, IllustrationIconNumbers, IllustrationIconText, IllustrationIconToggle, @@ -12,28 +10,23 @@ export interface OutputSchemaField { id: string; name: string; description?: string; - type: InputSchemaPropertyType | undefined; + type: AgentResponseFieldType | undefined; } export const OUTPUT_FIELD_TYPE_OPTIONS = [ { label: msg`Text`, - value: FieldMetadataType.TEXT, + value: 'string' as const, Icon: IllustrationIconText, }, { label: msg`Number`, - value: FieldMetadataType.NUMBER, + value: 'number' as const, Icon: IllustrationIconNumbers, }, { label: msg`Boolean`, - value: FieldMetadataType.BOOLEAN, + value: 'boolean' as const, Icon: IllustrationIconToggle, }, - { - label: msg`Date`, - value: FieldMetadataType.DATE, - Icon: IllustrationIconCalendarEvent, - }, ]; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts b/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts deleted file mode 100644 index d5035d46218..00000000000 --- a/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { type OutputSchemaField } from '@/ai/constants/OutputFieldTypeOptions'; -import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow'; -import { type AiAgentOutputSchema } from '@/workflow/workflow-variables/types/AiAgentOutputSchema'; -import { useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { useDebouncedCallback } from 'use-debounce'; -import { v4 } from 'uuid'; - -export const useAiAgentOutputSchema = ( - outputSchema?: AiAgentOutputSchema, - onActionUpdate?: (action: WorkflowAiAgentAction) => void, - action?: WorkflowAiAgentAction, - readonly?: boolean, -) => { - const [outputFields, setOutputFields] = useState( - Object.entries(outputSchema || {}).map(([name, field]) => ({ - id: v4(), - name, - type: field.type, - })), - ); - - const debouncedSave = useDebouncedCallback( - async (fields: OutputSchemaField[]) => { - if (readonly === true) { - return; - } - - const newOutputSchema = fields.reduce( - (schema, field) => { - if (isDefined(field.name)) { - schema[field.name] = { - isLeaf: true, - type: field.type, - value: null, - label: field.name, - }; - } - return schema; - }, - {}, - ); - - if (isDefined(onActionUpdate) && isDefined(action)) { - onActionUpdate({ - ...action, - settings: { - ...action.settings, - outputSchema: newOutputSchema, - }, - }); - } - }, - 500, - ); - - const handleOutputSchemaChange = async (fields: OutputSchemaField[]) => { - setOutputFields(fields); - await debouncedSave(fields); - }; - - return { - handleOutputSchemaChange, - outputFields, - }; -}; diff --git a/packages/twenty-front/src/modules/ai/types/AgentResponseFormat.ts b/packages/twenty-front/src/modules/ai/types/AgentResponseFormat.ts new file mode 100644 index 00000000000..84f422fde97 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/types/AgentResponseFormat.ts @@ -0,0 +1,8 @@ +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; + +export type AgentResponseFormat = + | { type: 'text' } + | { + type: 'json'; + schema: BaseOutputSchemaV2; + }; diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/getFieldIcon.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/getFieldIcon.test.ts index 099912b74d5..df4110c363d 100644 --- a/packages/twenty-front/src/modules/ai/utils/__tests__/getFieldIcon.test.ts +++ b/packages/twenty-front/src/modules/ai/utils/__tests__/getFieldIcon.test.ts @@ -1,19 +1,15 @@ -import { FieldMetadataType } from 'twenty-shared/types'; import { getFieldIcon } from '../getFieldIcon'; describe('getFieldIcon', () => { describe('supported field types', () => { - it('should return IconAbc for TEXT field type', () => { - expect(getFieldIcon(FieldMetadataType.TEXT)).toBe('IconAbc'); + it('should return IconAbc for string field type', () => { + expect(getFieldIcon('string')).toBe('IconAbc'); }); - it('should return IconText for NUMBER field type', () => { - expect(getFieldIcon(FieldMetadataType.NUMBER)).toBe('IconText'); + it('should return IconText for number field type', () => { + expect(getFieldIcon('number')).toBe('IconText'); }); - it('should return IconCheckbox for BOOLEAN field type', () => { - expect(getFieldIcon(FieldMetadataType.BOOLEAN)).toBe('IconCheckbox'); - }); - it('should return IconCalendarEvent for DATE field type', () => { - expect(getFieldIcon(FieldMetadataType.DATE)).toBe('IconCalendarEvent'); + it('should return IconCheckbox for boolean field type', () => { + expect(getFieldIcon('boolean')).toBe('IconCheckbox'); }); }); @@ -36,8 +32,8 @@ describe('getFieldIcon', () => { describe('consistency', () => { it('should return the same icon for the same field type', () => { - const result1 = getFieldIcon(FieldMetadataType.TEXT); - const result2 = getFieldIcon(FieldMetadataType.TEXT); + const result1 = getFieldIcon('string'); + const result2 = getFieldIcon('string'); expect(result1).toBe(result2); }); }); diff --git a/packages/twenty-front/src/modules/ai/utils/getFieldIcon.ts b/packages/twenty-front/src/modules/ai/utils/getFieldIcon.ts index ca5a6ab6110..71994e44019 100644 --- a/packages/twenty-front/src/modules/ai/utils/getFieldIcon.ts +++ b/packages/twenty-front/src/modules/ai/utils/getFieldIcon.ts @@ -1,16 +1,13 @@ -import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { type AgentResponseFieldType } from 'twenty-shared/ai'; -export const getFieldIcon = (fieldType?: InputSchemaPropertyType): string => { +export const getFieldIcon = (fieldType?: AgentResponseFieldType): string => { switch (fieldType) { - case FieldMetadataType.TEXT: + case 'string': return 'IconAbc'; - case FieldMetadataType.NUMBER: + case 'number': return 'IconText'; - case FieldMetadataType.BOOLEAN: + case 'boolean': return 'IconCheckbox'; - case FieldMetadataType.DATE: - return 'IconCalendarEvent'; default: return 'IconQuestionMark'; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx index 8dfd1c283d0..c8aa6cd5ce5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx @@ -1,28 +1,30 @@ -import { useAiAgentOutputSchema } from '@/ai/hooks/useAiAgentOutputSchema'; +import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions'; import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; import { Select } from '@/ui/input/components/Select'; +import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; +import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep'; import { AI_AGENT_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/AiAgentAction'; import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; -import { type AiAgentOutputSchema } from '@/workflow/workflow-variables/types/AiAgentOutputSchema'; -import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; +import { + type AgentResponseSchema, + type ModelConfiguration, +} from 'twenty-shared/ai'; +import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; -import { type SelectOption } from 'twenty-ui/input'; -import { useFindManyAgentsQuery } from '~/generated-metadata/graphql'; +import { useDebouncedCallback } from 'use-debounce'; +import { + useFindOneAgentQuery, + useUpdateOneAgentMutation, +} from '~/generated-metadata/graphql'; import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader'; -import { WorkflowOutputSchemaBuilder } from './WorkflowOutputSchemaBuilder'; - -const StyledErrorMessage = styled.div` - color: ${({ theme }) => theme.font.color.danger}; - font-size: ${({ theme }) => theme.font.size.sm}; - font-weight: ${({ theme }) => theme.font.weight.regular}; - margin-top: ${({ theme }) => theme.spacing(1)}; -`; +import { SettingsAgentModelCapabilities } from '~/pages/settings/ai/components/SettingsAgentModelCapabilities'; +import { SettingsAgentResponseFormat } from '~/pages/settings/ai/components/SettingsAgentResponseFormat'; type WorkflowEditActionAiAgentProps = { action: WorkflowAiAgentAction; @@ -45,62 +47,136 @@ export const WorkflowEditActionAiAgent = ({ defaultTitle: AI_AGENT_ACTION.defaultLabel, }); - const { handleOutputSchemaChange, outputFields } = useAiAgentOutputSchema( - action.settings.outputSchema as AiAgentOutputSchema, - actionOptions.readonly === true ? undefined : actionOptions.onActionUpdate, - action, - actionOptions.readonly, - ); + const agentId = action.settings.input.agentId; + const { data: agentData, loading: agentLoading } = useFindOneAgentQuery({ + variables: { id: agentId || '' }, + skip: !agentId, + }); + const [updateAgent] = useUpdateOneAgentMutation(); + const aiModelOptions = useAiModelOptions(); + const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep(); + const flow = useFlowOrThrow(); - const { data: agentsData, loading: agentsLoading } = useFindManyAgentsQuery(); + const agent = agentData?.findOneAgent; - const agentOptions = (agentsData?.findManyAgents || []).reduce< - SelectOption[] - >( - (acc, agent) => { - acc.push({ - label: agent.label, - value: agent.id, - Icon: agent.icon ? getIcon(agent.icon) : undefined, + const handleAgentPromptChange = useDebouncedCallback( + async (newPrompt: string) => { + if (actionOptions.readonly === true || !isDefined(agent)) { + return; + } + + await updateAgent({ + variables: { + input: { + id: agent.id, + prompt: newPrompt, + }, + }, + refetchQueries: ['FindOneAgent'], }); - return acc; }, - [ - { - label: t`No Agent`, - value: '', - }, - ], + 500, ); - const noAgentsAvailable = agentOptions.length === 0; - - const handleFieldChange = (field: 'agentId' | 'prompt', value: string) => { - if (actionOptions.readonly === true) { + const handleAgentModelChange = async (modelId: string) => { + if (actionOptions.readonly === true || !isDefined(agent)) { return; } - actionOptions.onActionUpdate?.({ - ...action, - settings: { - ...action.settings, + + await updateAgent({ + variables: { input: { - ...action.settings.input, - [field]: value, + id: agent.id, + modelId, }, }, + refetchQueries: ['FindOneAgent'], }); }; - return agentsLoading ? ( + const handleModelConfigurationChange = async ( + configuration: ModelConfiguration, + ) => { + if (actionOptions.readonly === true || !isDefined(agent)) { + return; + } + + await updateAgent({ + variables: { + input: { + id: agent.id, + modelConfiguration: configuration, + }, + }, + refetchQueries: ['FindOneAgent'], + }); + }; + + const updateAgentResponseFormat = async (format: { + type: 'text' | 'json'; + schema?: AgentResponseSchema; + }) => { + if (actionOptions.readonly === true || !isDefined(agent)) { + return; + } + + await updateAgent({ + variables: { + input: { + id: agent.id, + responseFormat: format, + }, + }, + refetchQueries: ['FindOneAgent'], + }); + + await updateWorkflowVersionStep({ + workflowVersionId: flow.workflowVersionId, + step: action, + }); + }; + + const debouncedUpdateAgentResponseFormat = useDebouncedCallback( + updateAgentResponseFormat, + 300, + ); + + const handleAgentResponseFormatChange = async (format: { + type: 'text' | 'json'; + schema?: AgentResponseSchema; + }) => { + if (format.type !== agent?.responseFormat?.type) { + debouncedUpdateAgentResponseFormat.cancel(); + void updateAgentResponseFormat(format); + } else { + void debouncedUpdateAgentResponseFormat(format); + } + }; + + return agentLoading ? ( ) : ( <> { + onTitleChange={async (newName: string) => { if (actionOptions.readonly === true) { return; } + actionOptions.onActionUpdate?.({ ...action, name: newName }); + + // Also update agent label + if (isDefined(agent)) { + await updateAgent({ + variables: { + input: { + id: agent.id, + label: newName, + }, + }, + refetchQueries: ['FindOneAgent'], + }); + } }} Icon={getIcon(headerIcon)} iconColor={headerIconColor} @@ -110,38 +186,43 @@ export const WorkflowEditActionAiAgent = ({ iconTooltip={AI_AGENT_ACTION.defaultLabel} /> -
- + + + + + + )} {!actionOptions.readonly && } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputFieldTypeSelector.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputFieldTypeSelector.tsx index a67f41b496a..cd122854380 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputFieldTypeSelector.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputFieldTypeSelector.tsx @@ -1,11 +1,11 @@ import { OUTPUT_FIELD_TYPE_OPTIONS } from '@/ai/constants/OutputFieldTypeOptions'; import { Select } from '@/ui/input/components/Select'; -import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; import { t } from '@lingui/core/macro'; +import { type AgentResponseFieldType } from 'twenty-shared/ai'; type WorkflowOutputFieldTypeSelectorProps = { - value?: InputSchemaPropertyType; - onChange: (value: InputSchemaPropertyType) => void; + value?: AgentResponseFieldType; + onChange: (value: AgentResponseFieldType) => void; disabled?: boolean; dropdownId: string; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx index 50abf41df54..12865657530 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder.tsx @@ -3,7 +3,6 @@ import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/c import { type OutputSchemaField } from '@/ai/constants/OutputFieldTypeOptions'; import { InputLabel } from '@/ui/input/components/InputLabel'; -import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; @@ -114,7 +113,7 @@ export const WorkflowOutputSchemaBuilder = ({ id: v4(), name: '', description: '', - type: 'TEXT' as InputSchemaPropertyType, + type: 'string', }; onChange([...fields, newField]); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/AiAgentOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/AiAgentOutputSchema.ts deleted file mode 100644 index f07f5c7575c..00000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/AiAgentOutputSchema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; - -export type AiAgentLeaf = { - isLeaf: true; - type: InputSchemaPropertyType | undefined; - label: string; - value: any; -}; - -export type AiAgentNode = { - isLeaf: false; - type: 'object' | 'unknown'; - label: string; - value: AiAgentOutputSchema; -}; - -export type AiAgentOutputSchema = Record; diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx index 57a3c1c6558..dbab6a5444b 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx @@ -92,6 +92,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { prompt: agent.prompt, isCustom: agent.isCustom, modelConfiguration: agent.modelConfiguration || {}, + responseFormat: agent.responseFormat || { type: 'text', schema: {} }, }); } else { enqueueErrorSnackBar({ @@ -190,6 +191,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { roleId: formValues.role, prompt: formValues.prompt, modelConfiguration: formValues.modelConfiguration, + responseFormat: formValues.responseFormat, }; await createAgent({ @@ -215,6 +217,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { roleId: formValues.role, prompt: formValues.prompt, modelConfiguration: formValues.modelConfiguration, + responseFormat: formValues.responseFormat, }, }, }); diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx index 4d7f36d5e3e..ad72da7b027 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx @@ -3,6 +3,9 @@ import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; import { Table } from '@/ui/layout/table/components/Table'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; @@ -10,7 +13,15 @@ import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; import { useTheme } from '@emotion/react'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; -import { H2Title, IconChevronRight, IconSearch } from 'twenty-ui/display'; +import { + H2Title, + IconChevronRight, + IconFilter, + IconSearch, + IconSettingsAutomation, +} from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; +import { MenuItemToggle } from 'twenty-ui/navigation'; import { SETTINGS_AI_AGENT_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsAiAgentTableMetadata'; import { normalizeSearchText } from '~/utils/normalizeSearchText'; @@ -22,11 +33,17 @@ import { StyledAIAgentTableRow, } from './SettingsAIAgentTableRow'; -const StyledSearchInput = styled(SettingsTextInput)` - padding-bottom: ${({ theme }) => theme.spacing(2)}; +const StyledSearchAndFilterContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; width: 100%; `; +const StyledSearchInput = styled(SettingsTextInput)` + flex: 1; +`; + const StyledTable = styled(Table)` margin-top: ${({ theme }) => theme.spacing(3)}; `; @@ -36,7 +53,7 @@ const StyledTableHeaderRow = styled(StyledAIAgentTableRow)` `; export const SettingsAIAgentsTable = ({ - withSearchBar = false, + withSearchBar = true, }: { withSearchBar?: boolean; }) => { @@ -45,6 +62,7 @@ export const SettingsAIAgentsTable = ({ const { t } = useLingui(); const theme = useTheme(); const [searchTerm, setSearchTerm] = useState(''); + const [showWorkflowAgents, setShowWorkflowAgents] = useState(false); const sortedAgents = useSortedArray( data?.findManyAgents || [], @@ -52,11 +70,15 @@ export const SettingsAIAgentsTable = ({ ); const filteredAgents = sortedAgents.filter((agent) => { + const isWorkflowAgent = agent.name.includes('workflow-service-agent'); + const matchesType = !isWorkflowAgent || showWorkflowAgents; + const searchNormalized = normalizeSearchText(searchTerm); - return ( + const matchesSearch = normalizeSearchText(agent.name).includes(searchNormalized) || - normalizeSearchText(agent.label).includes(searchNormalized) - ); + normalizeSearchText(agent.label).includes(searchNormalized); + + return matchesType && matchesSearch; }); return ( @@ -67,13 +89,44 @@ export const SettingsAIAgentsTable = ({ /> {withSearchBar && ( - + + + + } + dropdownComponents={ + + + + setShowWorkflowAgents(!showWorkflowAgents) + } + toggled={showWorkflowAgents} + text={t`Workflow agents`} + toggleSize="small" + /> + + + } + /> + )} diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentResponseFormat.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentResponseFormat.tsx new file mode 100644 index 00000000000..d8be7401498 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentResponseFormat.tsx @@ -0,0 +1,131 @@ +import { type OutputSchemaField } from '@/ai/constants/OutputFieldTypeOptions'; +import { Select } from '@/ui/input/components/Select'; +import { WorkflowOutputSchemaBuilder } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowOutputSchemaBuilder'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useState } from 'react'; +import { + type AgentResponseFieldType, + type AgentResponseSchema, +} from 'twenty-shared/ai'; +import { v4 } from 'uuid'; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +type SettingsAgentResponseFormatProps = { + responseFormat?: { + type: 'text' | 'json'; + schema?: AgentResponseSchema; + }; + onResponseFormatChange: (format: { + type: 'text' | 'json'; + schema?: AgentResponseSchema; + }) => void; + disabled?: boolean; +}; + +const schemaToFields = (schema: AgentResponseSchema): OutputSchemaField[] => { + if (!schema.properties) return []; + + return Object.entries(schema.properties).map(([key, field]) => ({ + id: v4(), + name: key, + description: field.description || '', + type: field.type, + })); +}; + +const fieldsToSchema = (fields: OutputSchemaField[]): AgentResponseSchema => { + const properties: Record< + string, + { type: AgentResponseFieldType; description?: string } + > = {}; + const required: string[] = []; + + fields + .filter((field) => field.name.trim() && field.type) + .forEach((field) => { + properties[field.name] = { + type: field.type!, + description: field.description || field.name, + }; + required.push(field.name); + }); + + return { + type: 'object' as const, + properties, + required, + additionalProperties: false as const, + }; +}; + +export const SettingsAgentResponseFormat = ({ + responseFormat, + onResponseFormatChange, + disabled, +}: SettingsAgentResponseFormatProps) => { + const formatType = responseFormat?.type || 'text'; + const schema: AgentResponseSchema = responseFormat?.schema || { + type: 'object' as const, + properties: {}, + required: [], + additionalProperties: false as const, + }; + + const [visualBuilderFields, setVisualBuilderFields] = useState< + OutputSchemaField[] + >(() => schemaToFields(schema)); + + const handleFormatTypeChange = (newType: 'text' | 'json') => { + if (newType === 'json') { + setVisualBuilderFields(schemaToFields(schema)); + } + const emptySchema: AgentResponseSchema = { + type: 'object' as const, + properties: {}, + required: [], + additionalProperties: false as const, + }; + onResponseFormatChange({ + type: newType, + schema: newType === 'text' ? emptySchema : schema, + }); + }; + + const handleVisualBuilderChange = (updatedFields: OutputSchemaField[]) => { + setVisualBuilderFields(updatedFields); + onResponseFormatChange({ + type: 'json', + schema: fieldsToSchema(updatedFields), + }); + }; + + return ( + +