feat: Add AI Agent workflow action node (#12650)

https://github.com/user-attachments/assets/8593e488-cb00-4fd2-b903-5ba5766e0254

---------

Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Co-authored-by: Joseph Chiang <josephj6802@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Ajay A Adsule <103304466+AjayAdsule@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Marty <91310557+real-marty@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-06-23 01:12:04 +05:30
committed by GitHub
parent 22e126869c
commit 65df511179
75 changed files with 2268 additions and 30 deletions

View File

@ -5,6 +5,7 @@ export type Leaf = {
type?: InputSchemaPropertyType;
icon?: string;
label?: string;
description?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
};
@ -14,6 +15,7 @@ export type Node = {
type?: InputSchemaPropertyType;
icon?: string;
label?: string;
description?: string;
value: OutputSchema;
};

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
@ -11,6 +12,7 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
@Module({
imports: [
AgentModule,
WorkflowSchemaModule,
ServerlessFunctionModule,
WorkflowRunnerModule,

View File

@ -8,6 +8,7 @@ import { v4 } from 'uuid';
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';
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -50,6 +51,7 @@ export class WorkflowVersionStepWorkspaceService {
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly agentService: AgentService,
@InjectRepository(ObjectMetadataEntity, 'core')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
@ -350,11 +352,13 @@ export class WorkflowVersionStepWorkspaceService {
}): Promise<WorkflowAction> {
// We don't enrich on the fly for code and HTTP request workflow actions.
// For code actions, OutputSchema is computed and updated when testing the serverless function.
// For HTTP requests, OutputSchema is determined by the expamle response input
// For HTTP requests and AI agent, OutputSchema is determined by the expamle response input
if (
[WorkflowActionType.CODE, WorkflowActionType.HTTP_REQUEST].includes(
step.type,
)
[
WorkflowActionType.CODE,
WorkflowActionType.HTTP_REQUEST,
WorkflowActionType.AI_AGENT,
].includes(step.type)
) {
return step;
}
@ -396,6 +400,17 @@ export class WorkflowVersionStepWorkspaceService {
}
break;
}
case WorkflowActionType.AI_AGENT: {
const agent = await this.agentService.findOneAgent(
step.settings.input.agentId,
workspaceId,
);
if (agent) {
await this.agentService.deleteOneAgent(agent.id, workspaceId);
}
break;
}
}
}
@ -578,6 +593,37 @@ export class WorkflowVersionStepWorkspaceService {
},
};
}
case WorkflowActionType.AI_AGENT: {
const newAgent = await this.agentService.createOneAgent(
{
name: 'AI Agent Workflow Step',
description: 'Created automatically for workflow step',
prompt: '',
modelId: 'gpt-4o',
},
workspaceId,
);
if (!isDefined(newAgent)) {
throw new WorkflowVersionStepException(
'Failed to create AI Agent Step',
WorkflowVersionStepExceptionCode.FAILURE,
);
}
return {
id: newStepId,
name: 'AI Agent',
type: WorkflowActionType.AI_AGENT,
valid: false,
settings: {
...BASE_STEP_DEFINITION,
input: {
agentId: newAgent.id,
},
},
};
}
default:
throw new WorkflowVersionStepException(
`WorkflowActionType '${type}' unknown`,

View File

@ -6,6 +6,7 @@ import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { AiAgentWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action';
import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action';
import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action';
import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action';
@ -29,6 +30,7 @@ export class WorkflowExecutorFactory {
private readonly formWorkflowAction: FormWorkflowAction,
private readonly filterWorkflowAction: FilterWorkflowAction,
private readonly httpRequestWorkflowAction: HttpRequestWorkflowAction,
private readonly aiAgentWorkflowAction: AiAgentWorkflowAction,
) {}
get(stepType: WorkflowActionType): WorkflowExecutor {
@ -51,6 +53,8 @@ export class WorkflowExecutorFactory {
return this.filterWorkflowAction;
case WorkflowActionType.HTTP_REQUEST:
return this.httpRequestWorkflowAction;
case WorkflowActionType.AI_AGENT:
return this.aiAgentWorkflowAction;
default:
throw new WorkflowStepExecutorException(
`Workflow step executor not found for step type '${stepType}'`,

View File

@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AiDriver } from 'src/engine/core-modules/ai/interfaces/ai.interface';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { AiAgentWorkflowAction } from './ai-agent.workflow-action';
@Module({
imports: [
AgentModule,
AiModule.forRoot({
useFactory: () => ({ type: AiDriver.OPENAI }),
}),
TypeOrmModule.forFeature([AgentEntity], 'core'),
],
providers: [ScopedWorkspaceContextFactory, AiAgentWorkflowAction],
exports: [AiAgentWorkflowAction],
})
export class AiAgentActionModule {}

View File

@ -0,0 +1,98 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
import { AgentExecutionService } from 'src/engine/metadata-modules/agent/agent-execution.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/agent/agent.exception';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowExecutorInput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
import { WorkflowExecutorOutput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-output.type';
import { isWorkflowAiAgentAction } from './guards/is-workflow-ai-agent-action.guard';
@Injectable()
export class AiAgentWorkflowAction implements WorkflowExecutor {
constructor(
private readonly agentExecutionService: AgentExecutionService,
private readonly aiBillingService: AIBillingService,
@InjectRepository(AgentEntity, 'core')
private readonly agentRepository: Repository<AgentEntity>,
) {}
async execute({
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowAiAgentAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not an AI Agent action',
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
const { agentId } = step.settings.input;
const workspaceId = context.workspaceId as string;
try {
const agent = await this.agentRepository.findOne({
where: {
id: agentId,
workspaceId,
},
});
if (!agent) {
throw new AgentException(
`Agent with id ${agentId} not found`,
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
const executionResult = await this.agentExecutionService.executeAgent({
agent,
context,
schema: step.settings.outputSchema,
});
await this.aiBillingService.calculateAndBillUsage(
agent.modelId,
executionResult.usage,
workspaceId,
);
return { result: executionResult.object };
} catch (error) {
if (error instanceof AgentException) {
return {
error: `${error.message} (${error.code})`,
};
}
return {
error:
error instanceof Error ? error.message : 'AI Agent execution failed',
};
}
}
}

View File

@ -0,0 +1,11 @@
import {
WorkflowAction,
WorkflowActionType,
WorkflowAiAgentAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const isWorkflowAiAgentAction = (
action: WorkflowAction,
): action is WorkflowAiAgentAction => {
return action.type === WorkflowActionType.AI_AGENT;
};

View File

@ -0,0 +1,3 @@
export type WorkflowAiAgentActionInput = {
agentId: string;
};

View File

@ -0,0 +1,6 @@
import { WorkflowAiAgentActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/types/workflow-ai-agent-action-input.type';
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
export type WorkflowAiAgentActionSettings = BaseWorkflowActionSettings & {
input: WorkflowAiAgentActionInput;
};

View File

@ -1,4 +1,5 @@
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
import { WorkflowAiAgentActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/types/workflow-ai-agent-action-settings.type';
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
@ -32,4 +33,5 @@ export type WorkflowActionSettings =
| WorkflowFindRecordsActionSettings
| WorkflowFormActionSettings
| WorkflowFilterActionSettings
| WorkflowHttpRequestActionSettings;
| WorkflowHttpRequestActionSettings
| WorkflowAiAgentActionSettings;

View File

@ -1,3 +1,4 @@
import { WorkflowAiAgentActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/types/workflow-ai-agent-action-settings.type';
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
@ -21,6 +22,7 @@ export enum WorkflowActionType {
FORM = 'FORM',
FILTER = 'FILTER',
HTTP_REQUEST = 'HTTP_REQUEST',
AI_AGENT = 'AI_AGENT',
}
type BaseWorkflowAction = {
@ -77,6 +79,11 @@ export type WorkflowHttpRequestAction = BaseWorkflowAction & {
settings: WorkflowHttpRequestActionSettings;
};
export type WorkflowAiAgentAction = BaseWorkflowAction & {
type: WorkflowActionType.AI_AGENT;
settings: WorkflowAiAgentActionSettings;
};
export type WorkflowAction =
| WorkflowCodeAction
| WorkflowSendEmailAction
@ -86,4 +93,5 @@ export type WorkflowAction =
| WorkflowFindRecordsAction
| WorkflowFormAction
| WorkflowFilterAction
| WorkflowHttpRequestAction;
| WorkflowHttpRequestAction
| WorkflowAiAgentAction;

View File

@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowExecutorFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-executor.factory';
import { AiAgentActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent-action.module';
import { CodeActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code-action.module';
import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module';
import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module';
@ -24,6 +26,8 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
BillingModule,
FilterActionModule,
HttpRequestActionModule,
AiAgentActionModule,
FeatureFlagModule,
],
providers: [
WorkflowExecutorWorkspaceService,