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
This commit is contained in:
Thomas Trompette
2026-02-26 17:17:48 +01:00
committed by GitHub
parent 81698ff32c
commit def5ea5764
6 changed files with 69 additions and 84 deletions
@@ -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}
/>
@@ -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 = ({
) : (
<WorkflowStepBody>
<WorkflowAiAgentPromptTab
prompt={prompt}
readonly={actionOptions.readonly === true}
aiModelOptions={aiModelOptions}
onPromptChange={handleAgentPromptChange}
@@ -44,15 +44,6 @@ describe('computeStepOutputSchema', () => {
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', () => {
@@ -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 {
@@ -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],
})
@@ -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<AgentEntity>,
) {}
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<WorkflowAction> {
const BACKEND_ENRICHED_TYPES = [
WorkflowActionType.AI_AGENT,
WorkflowActionType.ITERATOR,
];
const BACKEND_ENRICHED_TYPES = [WorkflowActionType.ITERATOR];
if (!BACKEND_ENRICHED_TYPES.includes(step.type)) {
return step;