From def5ea5764b5a243a7689f1f238da348ef2e37b7 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 26 Feb 2026 17:17:48 +0100 Subject: [PATCH] Fix ai agent node prompt and variables (#18275) - prompt stored on workflow lvl so input variables can be resolved and it can evolves with versions - make ai agent node output available as variables --- .../components/WorkflowAiAgentPromptTab.tsx | 4 +- .../components/WorkflowEditActionAiAgent.tsx | 49 ++++++++++--------- .../__tests__/computeStepOutputSchema.test.ts | 31 +++++++----- .../utils/generate/computeStepOutputSchema.ts | 12 ++++- .../workflow-schema/workflow-schema.module.ts | 8 +-- .../workflow-schema.workspace-service.ts | 49 ++++--------------- 6 files changed, 69 insertions(+), 84 deletions(-) diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPromptTab.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPromptTab.tsx index 21eafae73f..60934b98db 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPromptTab.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPromptTab.tsx @@ -13,6 +13,7 @@ import { SettingsAgentModelCapabilities } from '~/pages/settings/ai/components/S import { SettingsAgentResponseFormat } from '~/pages/settings/ai/components/SettingsAgentResponseFormat'; type WorkflowAiAgentPromptTabProps = { + prompt: string; readonly: boolean; aiModelOptions: SelectOption[]; onPromptChange: (value: string) => void; @@ -25,6 +26,7 @@ type WorkflowAiAgentPromptTabProps = { }; export const WorkflowAiAgentPromptTab = ({ + prompt, readonly, aiModelOptions, onPromptChange, @@ -42,7 +44,7 @@ export const WorkflowAiAgentPromptTab = ({ VariablePicker={WorkflowVariablePicker} label={t`Instructions for AI`} placeholder={t`Describe what you want the AI to do...`} - defaultValue={workflowAiAgentActionAgent?.prompt || ''} + defaultValue={prompt} onChange={onPromptChange} readonly={readonly} /> 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 24e05c91c5..5b5874d452 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 @@ -4,19 +4,21 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; 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 { WorkflowAiAgentPermissionsTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPermissionsTab'; +import { WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/WorkflowAiAgentTabListComponentId'; import { WORKFLOW_AI_AGENT_TABS } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/WorkflowAiAgentTabs'; import { useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState'; import { workflowAiAgentPermissionsIsAddingPermissionState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsIsAddingPermissionState'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; import { type AgentResponseSchema, type ModelConfiguration, @@ -32,7 +34,6 @@ import { } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader'; -import { WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/WorkflowAiAgentTabListComponentId'; import { WorkflowAiAgentPromptTab } from './WorkflowAiAgentPromptTab'; export type WorkflowAiAgentTabId = @@ -82,31 +83,30 @@ export const WorkflowEditActionAiAgent = ({ const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep(); const flow = useFlowOrThrow(); - const handleAgentPromptChange = useDebouncedCallback( - async (newPrompt: string) => { - if ( - actionOptions.readonly === true || - !isDefined(workflowAiAgentActionAgent) - ) { - return; - } + const actionPrompt = action.settings.input.prompt || ''; + const [prompt, setPrompt] = useState(actionPrompt); - const response = await updateAgent({ - variables: { - input: { - id: workflowAiAgentActionAgent.id, - prompt: newPrompt, - }, + const savePrompt = useDebouncedCallback((newPrompt: string) => { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + ...action.settings.input, + prompt: newPrompt, }, - }); + }, + }); + }, 500); - setWorkflowAiAgentActionAgent({ - ...workflowAiAgentActionAgent, - ...response.data?.updateOneAgent, - }); - }, - 500, - ); + const handleAgentPromptChange = (newPrompt: string) => { + setPrompt(newPrompt); + savePrompt(newPrompt); + }; const handleAgentModelChange = async (modelId: string) => { if ( @@ -291,6 +291,7 @@ export const WorkflowEditActionAiAgent = ({ ) : ( { expect(result).toBeUndefined(); }); - it('should return undefined for AI_AGENT step type', () => { - const result = computeStepOutputSchema({ - step: { type: 'AI_AGENT', settings: {} } as any, - objectMetadataItems: [], - }); - - expect(result).toBeUndefined(); - }); - it('should return undefined for WEBHOOK step type', () => { const result = computeStepOutputSchema({ step: { type: 'WEBHOOK', settings: {} } as any, @@ -417,6 +408,24 @@ describe('computeStepOutputSchema', () => { }); }); + describe('AI_AGENT step', () => { + it('should return response schema', () => { + const result = computeStepOutputSchema({ + step: { type: 'AI_AGENT', settings: {} } as any, + objectMetadataItems: [], + }); + + expect(result).toEqual({ + response: { + isLeaf: true, + type: FieldMetadataType.TEXT, + label: 'Response', + value: null, + }, + }); + }); + }); + describe('Empty output schema steps', () => { it.each(['FILTER', 'DELAY', 'EMPTY'])( 'should return empty object for %s step type', @@ -452,8 +461,8 @@ describe('shouldComputeOutputSchemaOnFrontend', () => { expect(shouldComputeOutputSchemaOnFrontend('HTTP_REQUEST')).toBe(false); }); - it('should return false for AI_AGENT', () => { - expect(shouldComputeOutputSchemaOnFrontend('AI_AGENT')).toBe(false); + it('should return true for AI_AGENT', () => { + expect(shouldComputeOutputSchemaOnFrontend('AI_AGENT')).toBe(true); }); it('should return false for WEBHOOK', () => { diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts index bf7dd13520..1326492fe9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts @@ -16,7 +16,6 @@ import { DatabaseEventAction } from '~/generated-metadata/graphql'; const PERSISTED_OUTPUT_SCHEMA_TYPES = [ 'CODE', 'HTTP_REQUEST', - 'AI_AGENT', 'WEBHOOK', 'ITERATOR', ]; @@ -191,6 +190,17 @@ export const computeStepOutputSchema = ({ return generateFormOutputSchema(formFields, objectMetadataItems); } + case 'AI_AGENT': { + return { + response: { + isLeaf: true, + type: FieldMetadataType.TEXT, + label: 'Response', + value: null, + }, + }; + } + case 'SEND_EMAIL': case 'DRAFT_EMAIL': { return { diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts index 3e0c29e5f7..57ab2aa72c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module.ts @@ -1,17 +1,11 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; @Module({ - imports: [ - WorkflowCommonModule, - FeatureFlagModule, - TypeOrmModule.forFeature([AgentEntity]), - ], + imports: [WorkflowCommonModule, FeatureFlagModule], providers: [WorkflowSchemaWorkspaceService], exports: [WorkflowSchemaWorkspaceService], }) diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index f9a65c7bf7..f218327fde 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { isString } from '@sniptt/guards'; import { isDefined, isValidVariable } from 'twenty-shared/utils'; @@ -12,11 +11,9 @@ import { SingleRecordAvailability, TRIGGER_STEP_ID, } from 'twenty-shared/workflow'; -import { Repository } from 'typeorm'; import { type DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action'; -import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const'; @@ -45,8 +42,6 @@ import { export class WorkflowSchemaWorkspaceService { constructor( private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, - @InjectRepository(AgentEntity) - private readonly agentRepository: Repository, ) {} async computeStepOutputSchema({ @@ -128,37 +123,14 @@ export class WorkflowSchemaWorkspaceService { }; } case WorkflowActionType.AI_AGENT: { - const agentId = step.settings.input.agentId; - - if (!isDefined(agentId) || agentId === '') { - return {}; - } - - const agent = await this.agentRepository.findOne({ - where: { id: agentId, workspaceId }, - }); - - if ( - !isDefined(agent) || - agent.responseFormat?.type !== 'json' || - !isDefined(agent.responseFormat.schema) - ) { - return {}; - } - - return Object.fromEntries( - Object.entries(agent.responseFormat.schema.properties).map( - ([key, field]) => [ - key, - { - isLeaf: true, - type: field.type, - label: field.description || key, - value: null, - }, - ], - ), - ) as OutputSchema; + return { + response: { + label: 'Response', + isLeaf: true, + type: 'string', + value: 'Response of the agent', + }, + }; } case WorkflowActionType.CODE: // StepOutput schema is computed on logicFunction draft execution default: @@ -175,10 +147,7 @@ export class WorkflowSchemaWorkspaceService { workspaceId: string; workflowVersionId: string; }): Promise { - const BACKEND_ENRICHED_TYPES = [ - WorkflowActionType.AI_AGENT, - WorkflowActionType.ITERATOR, - ]; + const BACKEND_ENRICHED_TYPES = [WorkflowActionType.ITERATOR]; if (!BACKEND_ENRICHED_TYPES.includes(step.type)) { return step;