Add preconfigured Workflow creation agent (#13855)
Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
+7
-2
@@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
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 { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
@@ -115,7 +115,12 @@ describe('WorkflowVersionStepWorkspaceService', () => {
|
||||
},
|
||||
},
|
||||
{ provide: ServerlessFunctionService, useValue: {} },
|
||||
{ provide: AgentService, useValue: {} },
|
||||
{
|
||||
provide: getRepositoryToken(AgentEntity, 'core'),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ObjectMetadataEntity, 'core'),
|
||||
useValue: {
|
||||
|
||||
+5
-3
@@ -2,7 +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 { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
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 { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
@@ -13,13 +13,15 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AgentModule,
|
||||
WorkflowSchemaModule,
|
||||
ServerlessFunctionModule,
|
||||
WorkflowRunnerModule,
|
||||
WorkflowRunModule,
|
||||
WorkflowCommonModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'core'),
|
||||
NestjsQueryTypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, AgentEntity],
|
||||
'core',
|
||||
),
|
||||
],
|
||||
providers: [WorkflowVersionStepWorkspaceService],
|
||||
exports: [WorkflowVersionStepWorkspaceService],
|
||||
|
||||
+7
-7
@@ -12,7 +12,7 @@ import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/se
|
||||
import { type CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
|
||||
import { type WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
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';
|
||||
@@ -54,7 +54,8 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,
|
||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||
private readonly agentService: AgentService,
|
||||
@InjectRepository(AgentEntity, 'core')
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
||||
@@ -433,13 +434,12 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
break;
|
||||
}
|
||||
|
||||
const agent = await this.agentService.findOneAgent(
|
||||
step.settings.input.agentId,
|
||||
workspaceId,
|
||||
);
|
||||
const agent = await this.agentRepository.findOne({
|
||||
where: { id: step.settings.input.agentId, workspaceId },
|
||||
});
|
||||
|
||||
if (isDefined(agent)) {
|
||||
await this.agentService.deleteOneAgent(agent.id, workspaceId);
|
||||
await this.agentRepository.delete({ id: agent.id, workspaceId });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+12
-4
@@ -3,18 +3,26 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
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 { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { AiAgentExecutorService } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service';
|
||||
|
||||
import { AiAgentWorkflowAction } from './ai-agent.workflow-action';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AgentModule,
|
||||
AiModule,
|
||||
TypeOrmModule.forFeature([AgentEntity], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[AgentEntity, RoleTargetsEntity, RoleEntity],
|
||||
'core',
|
||||
),
|
||||
],
|
||||
providers: [
|
||||
ScopedWorkspaceContextFactory,
|
||||
AiAgentWorkflowAction,
|
||||
AiAgentExecutorService,
|
||||
],
|
||||
providers: [ScopedWorkspaceContextFactory, AiAgentWorkflowAction],
|
||||
exports: [AiAgentWorkflowAction],
|
||||
})
|
||||
export class AiAgentActionModule {}
|
||||
|
||||
+10
-9
@@ -1,13 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { resolveInput } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.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,
|
||||
@@ -19,13 +18,14 @@ import {
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { AiAgentExecutorService } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service';
|
||||
|
||||
import { isWorkflowAiAgentAction } from './guards/is-workflow-ai-agent-action.guard';
|
||||
|
||||
@Injectable()
|
||||
export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly agentExecutionService: AgentExecutionService,
|
||||
private readonly aiAgentExecutionService: AiAgentExecutorService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
@InjectRepository(AgentEntity, 'core')
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@@ -74,12 +74,13 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const { result, usage } = await this.agentExecutionService.executeAgent({
|
||||
agent,
|
||||
context,
|
||||
schema: step.settings.outputSchema,
|
||||
userPrompt: resolveInput(prompt, context) as string,
|
||||
});
|
||||
const { result, usage } = await this.aiAgentExecutionService.executeAgent(
|
||||
{
|
||||
agent,
|
||||
schema: step.settings.outputSchema,
|
||||
userPrompt: resolveInput(prompt, context) as string,
|
||||
},
|
||||
);
|
||||
|
||||
await this.aiBillingService.calculateAndBillUsage(
|
||||
agent?.modelId ?? 'auto',
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { generateObject, generateText, ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentExecutionResult } 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 { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
|
||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
@Injectable()
|
||||
export class AiAgentExecutorService {
|
||||
private readonly logger = new Logger(AiAgentExecutorService.name);
|
||||
constructor(
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
private readonly toolService: ToolService,
|
||||
) {}
|
||||
|
||||
private async getTools(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolSet> {
|
||||
const roleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
agentId: agentId,
|
||||
workspaceId,
|
||||
},
|
||||
select: ['roleId'],
|
||||
});
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleTarget?.roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!roleTarget?.roleId || !role) {
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
|
||||
return { ...actionTools };
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
role.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
role.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
...databaseTools,
|
||||
...actionTools,
|
||||
};
|
||||
}
|
||||
|
||||
async executeAgent({
|
||||
agent,
|
||||
schema,
|
||||
userPrompt,
|
||||
}: {
|
||||
agent: AgentEntity | null;
|
||||
schema: OutputSchema;
|
||||
userPrompt: string;
|
||||
}): Promise<AgentExecutionResult> {
|
||||
try {
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
const tools = agent
|
||||
? await this.getTools(agent.id, agent.workspaceId)
|
||||
: {};
|
||||
|
||||
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
|
||||
|
||||
const textResponse = await generateText({
|
||||
system: `You are executing as part of a workflow automation. ${agent ? agent.prompt : ''}`,
|
||||
tools,
|
||||
model: registeredModel.model,
|
||||
prompt: userPrompt,
|
||||
maxSteps: AGENT_CONFIG.MAX_STEPS,
|
||||
});
|
||||
|
||||
if (Object.keys(schema).length === 0) {
|
||||
return {
|
||||
result: { response: textResponse.text },
|
||||
usage: textResponse.usage,
|
||||
};
|
||||
}
|
||||
const output = await generateObject({
|
||||
system: AGENT_SYSTEM_PROMPTS.OUTPUT_GENERATOR,
|
||||
model: registeredModel.model,
|
||||
prompt: `Based on the following execution results, generate the structured output according to the schema:
|
||||
|
||||
Execution Results: ${textResponse.text}
|
||||
|
||||
Please generate the structured output based on the execution results and context above.`,
|
||||
schema: convertOutputSchemaToZod(schema),
|
||||
});
|
||||
|
||||
return {
|
||||
result: output.object,
|
||||
usage: {
|
||||
promptTokens:
|
||||
(textResponse.usage?.promptTokens ?? 0) +
|
||||
(output.usage?.promptTokens ?? 0),
|
||||
completionTokens:
|
||||
(textResponse.usage?.completionTokens ?? 0) +
|
||||
(output.usage?.completionTokens ?? 0),
|
||||
totalTokens:
|
||||
(textResponse.usage?.totalTokens ?? 0) +
|
||||
(output.usage?.totalTokens ?? 0),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AgentException) {
|
||||
throw error;
|
||||
}
|
||||
throw new AgentException(
|
||||
error instanceof Error ? error.message : 'Agent execution failed',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
workflowActionSchema,
|
||||
workflowTriggerSchema,
|
||||
} from 'twenty-shared/workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const createWorkflowVersionStepSchema = z.object({
|
||||
workflowVersionId: z
|
||||
.string()
|
||||
.describe('The ID of the workflow version to add the step to'),
|
||||
stepType: z
|
||||
.enum(Object.values(WorkflowActionType) as [string, ...string[]])
|
||||
.describe('The type of step to create'),
|
||||
parentStepId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional ID of the parent step this step should come after'),
|
||||
nextStepId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional ID of the step this new step should connect to'),
|
||||
position: z
|
||||
.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
.optional()
|
||||
.describe('Optional position coordinates for the step'),
|
||||
});
|
||||
|
||||
export const updateWorkflowVersionStepSchema = z.object({
|
||||
workflowVersionId: z
|
||||
.string()
|
||||
.describe('The ID of the workflow version containing the step'),
|
||||
step: z
|
||||
.union([workflowTriggerSchema, workflowActionSchema])
|
||||
.describe('The updated step configuration'),
|
||||
});
|
||||
|
||||
export const deleteWorkflowVersionStepSchema = z.object({
|
||||
workflowVersionId: z
|
||||
.string()
|
||||
.describe('The ID of the workflow version containing the step'),
|
||||
stepId: z.string().describe('The ID of the step to delete'),
|
||||
});
|
||||
|
||||
export const createWorkflowVersionEdgeSchema = z.object({
|
||||
workflowVersionId: z.string().describe('The ID of the workflow version'),
|
||||
source: z.string().describe('The ID of the source step'),
|
||||
target: z.string().describe('The ID of the target step'),
|
||||
});
|
||||
|
||||
export const deleteWorkflowVersionEdgeSchema = z.object({
|
||||
workflowVersionId: z.string().describe('The ID of the workflow version'),
|
||||
source: z.string().describe('The ID of the source step'),
|
||||
target: z.string().describe('The ID of the target step'),
|
||||
});
|
||||
|
||||
export const createDraftFromWorkflowVersionSchema = z.object({
|
||||
workflowId: z.string().describe('The ID of the workflow'),
|
||||
workflowVersionIdToCopy: z
|
||||
.string()
|
||||
.describe('The ID of the workflow version to create a draft from'),
|
||||
});
|
||||
|
||||
export const updateWorkflowVersionPositionsSchema = z.object({
|
||||
workflowVersionId: z.string().describe('The ID of the workflow version'),
|
||||
positions: z
|
||||
.array(
|
||||
z.object({
|
||||
stepId: z.string(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.describe('Array of step positions to update'),
|
||||
});
|
||||
|
||||
export const activateWorkflowVersionSchema = z.object({
|
||||
workflowVersionId: z
|
||||
.string()
|
||||
.describe('The ID of the workflow version to activate'),
|
||||
});
|
||||
|
||||
export const deactivateWorkflowVersionSchema = z.object({
|
||||
workflowVersionId: z
|
||||
.string()
|
||||
.describe('The ID of the workflow version to deactivate'),
|
||||
});
|
||||
|
||||
export const computeStepOutputSchemaSchema = z.object({
|
||||
step: z
|
||||
.union([workflowTriggerSchema, workflowActionSchema])
|
||||
.describe('The workflow step configuration'),
|
||||
});
|
||||
|
||||
export const createCompleteWorkflowSchema = z.object({
|
||||
name: z.string().describe('The name of the workflow'),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional description of the workflow'),
|
||||
trigger: workflowTriggerSchema,
|
||||
steps: z
|
||||
.array(workflowActionSchema)
|
||||
.describe('Array of workflow action steps'),
|
||||
stepPositions: z
|
||||
.array(
|
||||
z.object({
|
||||
stepId: z
|
||||
.string()
|
||||
.describe('The ID of the step (use "trigger" for trigger step)'),
|
||||
position: z.object({
|
||||
x: z.number().describe('X coordinate for the step position'),
|
||||
y: z.number().describe('Y coordinate for the step position'),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe('Optional array of step positions for layout'),
|
||||
edges: z
|
||||
.array(
|
||||
z.object({
|
||||
source: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the source step (use "trigger" for trigger step)',
|
||||
),
|
||||
target: z.string().describe('The ID of the target step'),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe('Optional array of connections between steps'),
|
||||
activate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to activate the workflow immediately (default: false)'),
|
||||
});
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||
import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
|
||||
import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
|
||||
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
|
||||
import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import {
|
||||
activateWorkflowVersionSchema,
|
||||
computeStepOutputSchemaSchema,
|
||||
createCompleteWorkflowSchema,
|
||||
createDraftFromWorkflowVersionSchema,
|
||||
createWorkflowVersionEdgeSchema,
|
||||
createWorkflowVersionStepSchema,
|
||||
deactivateWorkflowVersionSchema,
|
||||
deleteWorkflowVersionEdgeSchema,
|
||||
deleteWorkflowVersionStepSchema,
|
||||
updateWorkflowVersionPositionsSchema,
|
||||
updateWorkflowVersionStepSchema,
|
||||
} from 'src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas';
|
||||
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowToolWorkspaceService {
|
||||
constructor(
|
||||
private readonly workflowVersionStepService: WorkflowVersionStepWorkspaceService,
|
||||
private readonly workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService,
|
||||
private readonly workflowVersionService: WorkflowVersionWorkspaceService,
|
||||
private readonly workflowTriggerService: WorkflowTriggerWorkspaceService,
|
||||
private readonly workflowSchemaService: WorkflowSchemaWorkspaceService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly recordPositionService: RecordPositionService,
|
||||
) {}
|
||||
|
||||
generateWorkflowTools(workspaceId: string, roleId: string): ToolSet {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
tools.create_complete_workflow = {
|
||||
description: `Create a complete workflow with trigger, steps, and connections in a single operation.
|
||||
|
||||
CRITICAL SCHEMA REQUIREMENTS:
|
||||
- Trigger type MUST be one of: DATABASE_EVENT, MANUAL, CRON, WEBHOOK
|
||||
- NEVER use "RECORD_CREATED" - this is invalid. Use "DATABASE_EVENT" instead.
|
||||
- Each step MUST include: id, name, type, valid, settings
|
||||
- CREATE_RECORD actions MUST have objectName and objectRecord in settings.input
|
||||
- objectRecord must contain actual field values, not just field names
|
||||
- Use "trigger" as stepId for trigger step in stepPositions and edges
|
||||
|
||||
Common mistakes to avoid:
|
||||
- Using "RECORD_CREATED" instead of "DATABASE_EVENT"
|
||||
- Missing the "name" and "valid" fields in steps
|
||||
- Missing the "objectRecord" field in CREATE_RECORD actions
|
||||
- Using "fieldsToUpdate" instead of "objectRecord" in CREATE_RECORD actions
|
||||
|
||||
IMPORTANT: The tool schema provides comprehensive field descriptions, examples, and validation rules. Always refer to the schema for:
|
||||
- Field requirements and data types
|
||||
- Common object patterns and field structures
|
||||
- Proper relationship field formats
|
||||
- Variable reference syntax (e.g., {{trigger.object.fieldName}})
|
||||
- Error handling options
|
||||
|
||||
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
|
||||
parameters: createCompleteWorkflowSchema,
|
||||
execute: async (parameters: {
|
||||
name: string;
|
||||
description?: string;
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowAction[];
|
||||
stepPositions?: Array<{
|
||||
stepId: string;
|
||||
position: { x: number; y: number };
|
||||
}>;
|
||||
edges?: Array<{ source: string; target: string }>;
|
||||
activate?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const workflowId = await this.createWorkflow({
|
||||
workspaceId,
|
||||
name: parameters.name,
|
||||
roleId,
|
||||
});
|
||||
|
||||
const workflowVersionId = await this.createWorkflowVersion({
|
||||
workspaceId,
|
||||
workflowId,
|
||||
trigger: parameters.trigger,
|
||||
steps: parameters.steps,
|
||||
roleId,
|
||||
});
|
||||
|
||||
if (parameters.stepPositions && parameters.stepPositions.length > 0) {
|
||||
const positions = parameters.stepPositions.map((pos) => ({
|
||||
id: pos.stepId === 'trigger' ? 'trigger' : pos.stepId,
|
||||
position: pos.position,
|
||||
}));
|
||||
|
||||
await this.workflowVersionService.updateWorkflowVersionPositions({
|
||||
workflowVersionId,
|
||||
positions,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (parameters.edges && parameters.edges.length > 0) {
|
||||
for (const edge of parameters.edges) {
|
||||
await this.workflowVersionEdgeService.createWorkflowVersionEdge({
|
||||
source: edge.source === 'trigger' ? 'trigger' : edge.source,
|
||||
target: edge.target,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (parameters.activate) {
|
||||
await this.workflowTriggerService.activateWorkflowVersion(
|
||||
workflowVersionId,
|
||||
);
|
||||
|
||||
await this.updateWorkflowStatus({
|
||||
workspaceId,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
name: parameters.name,
|
||||
trigger: parameters.trigger,
|
||||
steps: parameters.steps,
|
||||
message: `Workflow "${parameters.name}" created successfully with ${parameters.steps.length} steps`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to create workflow "${parameters.name}": ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.create_workflow_version_step = {
|
||||
description:
|
||||
'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration.',
|
||||
parameters: createWorkflowVersionStepSchema,
|
||||
execute: async (parameters: CreateWorkflowVersionStepInput) => {
|
||||
try {
|
||||
return await this.workflowVersionStepService.createWorkflowVersionStep(
|
||||
{
|
||||
workspaceId,
|
||||
input: parameters,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to create workflow version step: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.update_workflow_version_step = {
|
||||
description:
|
||||
'Update an existing step in a workflow version. This modifies the step configuration.',
|
||||
parameters: updateWorkflowVersionStepSchema,
|
||||
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
|
||||
try {
|
||||
return await this.workflowVersionStepService.updateWorkflowVersionStep(
|
||||
{
|
||||
workspaceId,
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
step: parameters.step,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to update workflow version step: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.delete_workflow_version_step = {
|
||||
description:
|
||||
'Delete a step from a workflow version. This removes the step and updates the workflow structure.',
|
||||
parameters: deleteWorkflowVersionStepSchema,
|
||||
execute: async (parameters: {
|
||||
workflowVersionId: string;
|
||||
stepId: string;
|
||||
}) => {
|
||||
try {
|
||||
return await this.workflowVersionStepService.deleteWorkflowVersionStep(
|
||||
{
|
||||
workspaceId,
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
stepIdToDelete: parameters.stepId,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to delete workflow version step: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.create_workflow_version_edge = {
|
||||
description:
|
||||
'Create a connection (edge) between two workflow steps. This defines the flow between steps.',
|
||||
parameters: createWorkflowVersionEdgeSchema,
|
||||
execute: async (parameters: {
|
||||
workflowVersionId: string;
|
||||
source: string;
|
||||
target: string;
|
||||
}) => {
|
||||
try {
|
||||
return await this.workflowVersionEdgeService.createWorkflowVersionEdge(
|
||||
{
|
||||
source: parameters.source,
|
||||
target: parameters.target,
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to create workflow version edge: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.delete_workflow_version_edge = {
|
||||
description: 'Delete a connection (edge) between workflow steps.',
|
||||
parameters: deleteWorkflowVersionEdgeSchema,
|
||||
execute: async (parameters: {
|
||||
workflowVersionId: string;
|
||||
source: string;
|
||||
target: string;
|
||||
}) => {
|
||||
try {
|
||||
return await this.workflowVersionEdgeService.deleteWorkflowVersionEdge(
|
||||
{
|
||||
source: parameters.source,
|
||||
target: parameters.target,
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to delete workflow version edge: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.create_draft_from_workflow_version = {
|
||||
description:
|
||||
'Create a new draft workflow version from an existing one. This allows for iterative workflow development.',
|
||||
parameters: createDraftFromWorkflowVersionSchema,
|
||||
execute: async (parameters: {
|
||||
workflowId: string;
|
||||
workflowVersionIdToCopy: string;
|
||||
}) => {
|
||||
try {
|
||||
return await this.workflowVersionService.createDraftFromWorkflowVersion(
|
||||
{
|
||||
workspaceId,
|
||||
workflowId: parameters.workflowId,
|
||||
workflowVersionIdToCopy: parameters.workflowVersionIdToCopy,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to create draft from workflow version: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.update_workflow_version_positions = {
|
||||
description:
|
||||
'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.',
|
||||
parameters: updateWorkflowVersionPositionsSchema,
|
||||
execute: async (parameters: UpdateWorkflowVersionPositionsInput) => {
|
||||
try {
|
||||
return await this.workflowVersionService.updateWorkflowVersionPositions(
|
||||
{
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
positions: parameters.positions,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to update workflow version step positions: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.activate_workflow_version = {
|
||||
description:
|
||||
'Activate a workflow version. This makes the workflow version active and available for execution.',
|
||||
parameters: activateWorkflowVersionSchema,
|
||||
execute: async (parameters: { workflowVersionId: string }) => {
|
||||
try {
|
||||
return await this.workflowTriggerService.activateWorkflowVersion(
|
||||
parameters.workflowVersionId,
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to activate workflow version: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.deactivate_workflow_version = {
|
||||
description:
|
||||
'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.',
|
||||
parameters: deactivateWorkflowVersionSchema,
|
||||
execute: async (parameters: { workflowVersionId: string }) => {
|
||||
try {
|
||||
return await this.workflowTriggerService.deactivateWorkflowVersion(
|
||||
parameters.workflowVersionId,
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to deactivate workflow version: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
tools.compute_step_output_schema = {
|
||||
description:
|
||||
'Compute the output schema for a workflow step. This determines what data the step produces. The step parameter must be a valid WorkflowTrigger or WorkflowAction with the correct settings structure for its type.',
|
||||
parameters: computeStepOutputSchemaSchema,
|
||||
execute: async (parameters: {
|
||||
step: WorkflowTrigger | WorkflowAction;
|
||||
}) => {
|
||||
try {
|
||||
return await this.workflowSchemaService.computeStepOutputSchema({
|
||||
step: parameters.step,
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: `Failed to compute step output schema: ${error.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
private async createWorkflow({
|
||||
workspaceId,
|
||||
name,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
roleId: string;
|
||||
}): Promise<string> {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ roleId },
|
||||
);
|
||||
|
||||
const workflowPosition =
|
||||
await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
isCustom: false,
|
||||
nameSingular: 'workflow',
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const workflow = workflowRepository.create({
|
||||
id: uuidv4(),
|
||||
name,
|
||||
statuses: [WorkflowStatus.DRAFT],
|
||||
position: workflowPosition,
|
||||
});
|
||||
|
||||
const savedWorkflow = await workflowRepository.save(workflow);
|
||||
|
||||
return savedWorkflow.id;
|
||||
}
|
||||
|
||||
private async createWorkflowVersion({
|
||||
workspaceId,
|
||||
workflowId,
|
||||
trigger,
|
||||
steps,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowId: string;
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowAction[];
|
||||
roleId: string;
|
||||
}): Promise<string> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ roleId },
|
||||
);
|
||||
|
||||
const versionPosition =
|
||||
await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
isCustom: false,
|
||||
nameSingular: 'workflowVersion',
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const workflowVersion = workflowVersionRepository.create({
|
||||
id: uuidv4(),
|
||||
workflowId,
|
||||
name: 'v1',
|
||||
status: WorkflowVersionStatus.DRAFT,
|
||||
trigger,
|
||||
steps,
|
||||
position: versionPosition,
|
||||
});
|
||||
|
||||
const savedWorkflowVersion =
|
||||
await workflowVersionRepository.save(workflowVersion);
|
||||
|
||||
return savedWorkflowVersion.id;
|
||||
}
|
||||
|
||||
private async updateWorkflowStatus({
|
||||
workspaceId,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
roleId: string;
|
||||
}) {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ roleId },
|
||||
);
|
||||
|
||||
await workflowRepository.update(workflowId, {
|
||||
statuses: [WorkflowStatus.ACTIVE],
|
||||
lastPublishedVersionId: workflowVersionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
|
||||
import { WorkflowVersionEdgeModule } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.module';
|
||||
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module';
|
||||
import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module';
|
||||
import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module';
|
||||
|
||||
import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace-service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkflowVersionStepModule,
|
||||
WorkflowVersionEdgeModule,
|
||||
WorkflowVersionModule,
|
||||
WorkflowTriggerModule,
|
||||
WorkflowSchemaModule,
|
||||
RecordPositionModule,
|
||||
],
|
||||
providers: [WorkflowToolWorkspaceService],
|
||||
exports: [WorkflowToolWorkspaceService],
|
||||
})
|
||||
export class WorkflowToolsModule {}
|
||||
Reference in New Issue
Block a user