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:
@@ -62,6 +62,7 @@ export type Agent = {
|
||||
prompt: Scalars['String'];
|
||||
responseFormat?: Maybe<Scalars['JSON']>;
|
||||
roleId?: Maybe<Scalars['UUID']>;
|
||||
standardId?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export type Agent = {
|
||||
prompt: Scalars['String'];
|
||||
responseFormat?: Maybe<Scalars['JSON']>;
|
||||
roleId?: Maybe<Scalars['UUID']>;
|
||||
standardId?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { type WorkflowRunState } from '@/workflow/types/Workflow';
|
||||
import { workflowRunStateSchema } from '@/workflow/validation-schemas/workflowSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { workflowRunStateSchema } from 'twenty-shared/workflow';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
export const orderWorkflowRunState = (value: JsonValue) => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { type WorkflowRun } from '@/workflow/types/Workflow';
|
||||
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { workflowRunSchema } from 'twenty-shared/workflow';
|
||||
|
||||
export const useWorkflowRun = ({
|
||||
workflowRunId,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type workflowTriggerSchema,
|
||||
type workflowUpdateRecordActionSchema,
|
||||
type workflowWebhookTriggerSchema,
|
||||
} from '@/workflow/validation-schemas/workflowSchema';
|
||||
} from 'twenty-shared/workflow';
|
||||
import { type z } from 'zod';
|
||||
|
||||
export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;
|
||||
|
||||
+4
-6
@@ -1,6 +1,7 @@
|
||||
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { StepLogicalOperator, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
@@ -42,9 +43,7 @@ const CONFIGURED_ACTION: WorkflowFilterAction = {
|
||||
stepFilterGroups: [
|
||||
{
|
||||
id: 'filter-group-1',
|
||||
parentStepFilterGroupId: null,
|
||||
logicalOperator: 'AND',
|
||||
stepFilterGroupChildren: [],
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
],
|
||||
stepFilters: [
|
||||
@@ -52,10 +51,9 @@ const CONFIGURED_ACTION: WorkflowFilterAction = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
operandType: 'LITERAL',
|
||||
operand: 'contains',
|
||||
operand: ViewFilterOperand.Contains,
|
||||
value: 'Acme',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+3
-3
@@ -3,13 +3,13 @@ import {
|
||||
type WorkflowHttpRequestAction,
|
||||
type WorkflowSendEmailAction,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
workflowFormActionSettingsSchema,
|
||||
workflowHttpRequestActionSettingsSchema,
|
||||
workflowSendEmailActionSettingsSchema,
|
||||
} from '@/workflow/validation-schemas/workflowSchema';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
} from 'twenty-shared/workflow';
|
||||
import { useWorkflowActionHeader } from '../useWorkflowActionHeader';
|
||||
|
||||
jest.mock('../useActionIconColorOrThrow', () => ({
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddStandardIdToAgent1754923039348 implements MigrationInterface {
|
||||
name = 'AddStandardIdToAgent1754923039348';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "core"."agent" ADD "standardId" uuid`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agent" DROP COLUMN "standardId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+35
@@ -182,4 +182,39 @@ export class AiModelRegistryService {
|
||||
refreshRegistry(): void {
|
||||
this.buildModelRegistry();
|
||||
}
|
||||
|
||||
async resolveModelForAgent(agent: { modelId: string } | null) {
|
||||
const aiModel = this.getEffectiveModelConfig(agent?.modelId ?? 'auto');
|
||||
|
||||
await this.validateApiKey(aiModel.provider);
|
||||
const registeredModel = this.getModel(aiModel.modelId);
|
||||
|
||||
if (!registeredModel) {
|
||||
throw new Error(`Model ${aiModel.modelId} not found in registry`);
|
||||
}
|
||||
|
||||
return registeredModel;
|
||||
}
|
||||
|
||||
async validateApiKey(provider: ModelProvider): Promise<void> {
|
||||
let apiKey: string | undefined;
|
||||
|
||||
switch (provider) {
|
||||
case ModelProvider.OPENAI:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
break;
|
||||
case ModelProvider.ANTHROPIC:
|
||||
apiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
||||
break;
|
||||
case ModelProvider.OPENAI_COMPATIBLE:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_COMPATIBLE_API_KEY');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`${provider.toUpperCase()} API key not configured`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
generateSoftDeleteToolSchema,
|
||||
getRecordInputSchema,
|
||||
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
|
||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util';
|
||||
import { isWorkflowRunObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-run-object.util';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
@@ -48,7 +48,7 @@ export class ToolService {
|
||||
});
|
||||
|
||||
const filteredObjectMetadata = allObjectMetadata.filter(
|
||||
(objectMetadata) => !isWorkflowRelatedObject(objectMetadata),
|
||||
(objectMetadata) => !isWorkflowRunObject(objectMetadata),
|
||||
);
|
||||
|
||||
filteredObjectMetadata.forEach((objectMetadata) => {
|
||||
|
||||
+22
-61
@@ -11,23 +11,22 @@ import {
|
||||
generateText,
|
||||
type ImagePart,
|
||||
streamText,
|
||||
ToolSet,
|
||||
type UserContent,
|
||||
} from 'ai';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
type AgentChatMessageEntity,
|
||||
AgentChatMessageRole,
|
||||
} from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
|
||||
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
|
||||
import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service';
|
||||
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 { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/agent/types/recordIdsByObjectMetadataNameSingular.type';
|
||||
@@ -37,6 +36,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
||||
import { type OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
import { AgentToolGeneratorService } from './agent-tool-generator.service';
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
|
||||
@@ -54,40 +54,20 @@ export class AgentExecutionService {
|
||||
private readonly logger = new Logger(AgentExecutionService.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly agentToolService: AgentToolService,
|
||||
private readonly agentHandoffToolService: AgentHandoffToolService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly agentToolGeneratorService: AgentToolGeneratorService,
|
||||
|
||||
@InjectRepository(AgentEntity, 'core')
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
@InjectRepository(FileEntity, 'core')
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
) {}
|
||||
|
||||
private async validateApiKey(provider: ModelProvider): Promise<void> {
|
||||
let apiKey: string | undefined;
|
||||
|
||||
switch (provider) {
|
||||
case ModelProvider.OPENAI:
|
||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||
break;
|
||||
case ModelProvider.ANTHROPIC:
|
||||
apiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (!apiKey) {
|
||||
throw new AgentException(
|
||||
`${provider.toUpperCase()} API key not configured`,
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async prepareAIRequestConfig({
|
||||
messages,
|
||||
prompt,
|
||||
@@ -106,48 +86,29 @@ export class AgentExecutionService {
|
||||
);
|
||||
}
|
||||
|
||||
const aiModel = this.aiModelRegistryService.getEffectiveModelConfig(
|
||||
agent?.modelId ?? 'auto',
|
||||
);
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
if (agent && !aiModel) {
|
||||
const error = `AI model with id ${agent.modelId} not found`;
|
||||
let tools: ToolSet = {};
|
||||
|
||||
this.logger.error(error);
|
||||
throw new AgentException(
|
||||
error,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Resolved model: ${aiModel.modelId} (provider: ${aiModel.provider})`,
|
||||
);
|
||||
|
||||
const provider = aiModel.provider;
|
||||
|
||||
await this.validateApiKey(provider);
|
||||
|
||||
const tools = agent
|
||||
? await this.agentToolService.generateToolsForAgent(
|
||||
if (agent) {
|
||||
const baseTools =
|
||||
await this.agentToolGeneratorService.generateToolsForAgent(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
)
|
||||
: {};
|
||||
);
|
||||
|
||||
const handoffTools =
|
||||
await this.agentHandoffToolService.generateHandoffTools(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
);
|
||||
|
||||
tools = { ...baseTools, ...handoffTools };
|
||||
}
|
||||
|
||||
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
|
||||
|
||||
const registeredModel = this.aiModelRegistryService.getModel(
|
||||
aiModel.modelId,
|
||||
);
|
||||
|
||||
if (!registeredModel) {
|
||||
throw new AgentException(
|
||||
`Model ${aiModel.modelId} not found in registry`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
system,
|
||||
tools,
|
||||
|
||||
+18
-18
@@ -1,13 +1,13 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { generateText } from 'ai';
|
||||
import { CoreMessage, generateText } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { AGENT_HANDOFF_PROMPT_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-prompt.const';
|
||||
|
||||
import { AgentHandoffService } from './agent-handoff.service';
|
||||
import { AgentToolGeneratorService } from './agent-tool-generator.service';
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
|
||||
@@ -15,8 +15,7 @@ export type HandoffRequest = {
|
||||
fromAgentId: string;
|
||||
toAgentId: string;
|
||||
workspaceId: string;
|
||||
reason: string;
|
||||
context?: string;
|
||||
messages?: CoreMessage[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -28,11 +27,12 @@ export class AgentHandoffExecutorService {
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly agentHandoffService: AgentHandoffService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly agentToolGeneratorService: AgentToolGeneratorService,
|
||||
) {}
|
||||
|
||||
async executeHandoff(handoffRequest: HandoffRequest) {
|
||||
try {
|
||||
const { fromAgentId, toAgentId, workspaceId } = handoffRequest;
|
||||
const { fromAgentId, toAgentId, workspaceId, messages } = handoffRequest;
|
||||
|
||||
const canHandoff = await this.agentHandoffService.canHandoffTo({
|
||||
fromAgentId,
|
||||
@@ -58,9 +58,8 @@ export class AgentHandoffExecutorService {
|
||||
);
|
||||
}
|
||||
|
||||
const registeredModel = this.aiModelRegistryService.getModel(
|
||||
targetAgent.modelId,
|
||||
);
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(targetAgent);
|
||||
|
||||
if (!registeredModel) {
|
||||
throw new AgentException(
|
||||
@@ -69,14 +68,24 @@ export class AgentHandoffExecutorService {
|
||||
);
|
||||
}
|
||||
|
||||
const tools = await this.agentToolGeneratorService.generateToolsForAgent(
|
||||
toAgentId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const aiRequestConfig = {
|
||||
system: targetAgent.prompt,
|
||||
prompt: this.createHandoffPrompt(handoffRequest),
|
||||
messages,
|
||||
tools,
|
||||
model: registeredModel.model,
|
||||
};
|
||||
|
||||
const textResponse = await generateText(aiRequestConfig);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully executed handoff to agent ${toAgentId} with response length: ${textResponse.text.length}`,
|
||||
);
|
||||
|
||||
return textResponse.text;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -92,13 +101,4 @@ export class AgentHandoffExecutorService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private createHandoffPrompt(handoffRequest: HandoffRequest): string {
|
||||
const { reason, context } = handoffRequest;
|
||||
|
||||
return AGENT_HANDOFF_PROMPT_TEMPLATE.replace('{reason}', reason).replace(
|
||||
'{context}',
|
||||
context || 'No additional context provided',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
|
||||
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
|
||||
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
|
||||
import { AGENT_HANDOFF_DESCRIPTION_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-description.const';
|
||||
import { AGENT_HANDOFF_SCHEMA } from 'src/engine/metadata-modules/agent/constants/agent-handoff-schema.const';
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
|
||||
@Injectable()
|
||||
export class AgentHandoffToolService {
|
||||
constructor(
|
||||
private readonly agentHandoffService: AgentHandoffService,
|
||||
private readonly agentHandoffExecutorService: AgentHandoffExecutorService,
|
||||
) {}
|
||||
|
||||
public async generateHandoffTools(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolSet> {
|
||||
const handoffs = await this.agentHandoffService.getAgentHandoffs({
|
||||
fromAgentId: agentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const handoffTools = handoffs.reduce<ToolSet>((tools, handoff) => {
|
||||
const toolName = `handoff_to_${camelCase(handoff.toAgent.name)}`;
|
||||
|
||||
tools[toolName] = {
|
||||
description:
|
||||
handoff.description ||
|
||||
handoff.toAgent.description ||
|
||||
AGENT_HANDOFF_DESCRIPTION_TEMPLATE.replace(
|
||||
'{agentName}',
|
||||
handoff.toAgent.name,
|
||||
),
|
||||
parameters: AGENT_HANDOFF_SCHEMA,
|
||||
execute: async ({ input }) => {
|
||||
const result = await this.agentHandoffExecutorService.executeHandoff({
|
||||
fromAgentId: agentId,
|
||||
toAgentId: handoff.toAgent.id,
|
||||
workspaceId,
|
||||
messages: input.messages,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
return tools;
|
||||
}, {});
|
||||
|
||||
return handoffTools;
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkflowToolWorkspaceService as WorkflowToolService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentToolGeneratorService {
|
||||
private readonly logger = new Logger(AgentToolGeneratorService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
private readonly toolService: ToolService,
|
||||
private readonly workflowToolService: WorkflowToolService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly agentService: AgentService,
|
||||
) {}
|
||||
|
||||
async generateToolsForAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolSet> {
|
||||
let tools: ToolSet = {};
|
||||
|
||||
try {
|
||||
const agent = await this.agentService.findOneAgent(agentId, workspaceId);
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
|
||||
tools = { ...actionTools };
|
||||
|
||||
const roleId = agent.roleId;
|
||||
|
||||
if (!roleId) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const hasWorkflowPermission =
|
||||
this.permissionsService.checkRolePermissions(
|
||||
role,
|
||||
PermissionFlagType.WORKFLOWS,
|
||||
);
|
||||
|
||||
if (hasWorkflowPermission) {
|
||||
const workflowTools = this.workflowToolService.generateWorkflowTools(
|
||||
workspaceId,
|
||||
roleId,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...workflowTools };
|
||||
}
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
roleId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...databaseTools };
|
||||
|
||||
const roleActionTools = await this.toolAdapterService.getTools(
|
||||
roleId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...roleActionTools };
|
||||
} catch (toolError) {
|
||||
this.logger.warn(
|
||||
`Failed to generate tools for agent ${agentId}: ${toolError.message}. Proceeding without tools.`,
|
||||
);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
|
||||
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AGENT_HANDOFF_DESCRIPTION_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-description.const';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
|
||||
@Injectable()
|
||||
export class AgentToolService {
|
||||
constructor(
|
||||
private readonly agentService: AgentService,
|
||||
private readonly agentHandoffService: AgentHandoffService,
|
||||
private readonly agentHandoffExecutorService: AgentHandoffExecutorService,
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
private readonly toolService: ToolService,
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
) {}
|
||||
|
||||
async generateToolsForAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolSet> {
|
||||
const agent = await this.agentService.findOneAgent(agentId, workspaceId);
|
||||
|
||||
const handoffTools = await this.generateHandoffTools(agentId, workspaceId);
|
||||
|
||||
if (!agent.roleId) {
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
|
||||
return { ...actionTools, ...handoffTools };
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: agent.roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
role.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
role.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return { ...databaseTools, ...actionTools, ...handoffTools };
|
||||
}
|
||||
|
||||
private async generateHandoffTools(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolSet> {
|
||||
const handoffs = await this.agentHandoffService.getAgentHandoffs({
|
||||
fromAgentId: agentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const handoffTools = handoffs.reduce<ToolSet>((tools, handoff) => {
|
||||
const toolName = `handoff_to_${camelCase(handoff.toAgent.name)}`;
|
||||
|
||||
const handoffSchema = z.object({
|
||||
toolDescription: z
|
||||
.string()
|
||||
.describe(
|
||||
"A clear, human-readable status message describing the handoff being made. This will be shown to the user while the handoff is being processed, so phrase it as a present-tense status update (e.g., 'Transferring you to the sales agent for pricing information').",
|
||||
),
|
||||
input: z.object({
|
||||
reason: z
|
||||
.string()
|
||||
.describe(
|
||||
'Brief explanation of why this handoff is needed (e.g., "User needs pricing information", "User requires technical support", "User wants to discuss billing")',
|
||||
),
|
||||
context: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Any relevant context or information to pass to the receiving agent (e.g., user preferences, previous conversation details, specific requirements)',
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
tools[toolName] = {
|
||||
description:
|
||||
handoff.description ||
|
||||
handoff.toAgent.description ||
|
||||
AGENT_HANDOFF_DESCRIPTION_TEMPLATE.replace(
|
||||
'{agentName}',
|
||||
handoff.toAgent.name,
|
||||
),
|
||||
parameters: handoffSchema,
|
||||
execute: async ({ input: { reason, context } }) => {
|
||||
const result = await this.agentHandoffExecutorService.executeHandoff({
|
||||
fromAgentId: agentId,
|
||||
toAgentId: handoff.toAgent.id,
|
||||
workspaceId,
|
||||
reason,
|
||||
context,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
return tools;
|
||||
}, {});
|
||||
|
||||
return handoffTools;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ export class AgentEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
standardId?: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
|
||||
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
|
||||
import { AgentChatController } from 'src/engine/metadata-modules/agent/agent-chat.controller';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module';
|
||||
|
||||
import { AgentChatMessageEntity } from './agent-chat-message.entity';
|
||||
import { AgentChatThreadEntity } from './agent-chat-thread.entity';
|
||||
@@ -25,11 +27,12 @@ import { AgentChatResolver } from './agent-chat.resolver';
|
||||
import { AgentChatService } from './agent-chat.service';
|
||||
import { AgentExecutionService } from './agent-execution.service';
|
||||
import { AgentHandoffExecutorService } from './agent-handoff-executor.service';
|
||||
import { AgentHandoffToolService } from './agent-handoff-tool.service';
|
||||
import { AgentHandoffEntity } from './agent-handoff.entity';
|
||||
import { AgentHandoffService } from './agent-handoff.service';
|
||||
import { AgentStreamingService } from './agent-streaming.service';
|
||||
import { AgentTitleGenerationService } from './agent-title-generation.service';
|
||||
import { AgentToolService } from './agent-tool.service';
|
||||
import { AgentToolGeneratorService } from './agent-tool-generator.service';
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentResolver } from './agent.resolver';
|
||||
import { AgentService } from './agent.service';
|
||||
@@ -57,10 +60,12 @@ import { AgentService } from './agent.service';
|
||||
FileUploadModule,
|
||||
FileModule,
|
||||
ObjectMetadataModule,
|
||||
PermissionsModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
TokenModule,
|
||||
DomainManagerModule,
|
||||
WorkflowToolsModule,
|
||||
],
|
||||
controllers: [AgentChatController],
|
||||
providers: [
|
||||
@@ -68,7 +73,8 @@ import { AgentService } from './agent.service';
|
||||
AgentChatResolver,
|
||||
AgentService,
|
||||
AgentExecutionService,
|
||||
AgentToolService,
|
||||
AgentToolGeneratorService,
|
||||
AgentHandoffToolService,
|
||||
AgentChatService,
|
||||
AgentStreamingService,
|
||||
AgentTitleGenerationService,
|
||||
@@ -78,7 +84,8 @@ import { AgentService } from './agent.service';
|
||||
exports: [
|
||||
AgentService,
|
||||
AgentExecutionService,
|
||||
AgentToolService,
|
||||
AgentToolGeneratorService,
|
||||
AgentHandoffToolService,
|
||||
AgentChatService,
|
||||
AgentStreamingService,
|
||||
AgentTitleGenerationService,
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export const AGENT_HANDOFF_DESCRIPTION_TEMPLATE =
|
||||
"Use this tool when the user's request requires {agentName}'s specialized expertise or capabilities. CRITICAL: You MUST call this tool function immediately. Do NOT respond with text about transferring - execute the tool instead. This is a FUNCTION CALL - you must invoke it, not describe it.";
|
||||
"Use this tool when the user's request requires {agentName}'s specialized expertise or capabilities. This will seamlessly consult with the specialist agent and provide you with their expert response to continue the conversation naturally. CRITICAL: You MUST call this tool function immediately. Do NOT respond with text about transferring - execute the tool instead. This is a FUNCTION CALL - you must invoke it, not describe it.";
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
export const AGENT_HANDOFF_PROMPT_TEMPLATE = `You have received a handoff from another AI agent who determined that you are better suited to handle this conversation based on your specialized knowledge and capabilities.
|
||||
|
||||
**Reason for handoff:** {reason}
|
||||
|
||||
**Context from the previous agent:**
|
||||
{context}
|
||||
|
||||
**Instructions:**
|
||||
- Continue the conversation naturally and professionally
|
||||
- Leverage your specialized expertise to provide the best possible assistance
|
||||
- Maintain context from the previous conversation while adding your unique value`;
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const AGENT_HANDOFF_SCHEMA = z.object({
|
||||
toolDescription: z
|
||||
.string()
|
||||
.describe(
|
||||
'A brief, user-friendly message explaining what is happening while the handoff is being processed. This will be shown to the user during the handoff execution.',
|
||||
),
|
||||
input: z.object({
|
||||
messages: z
|
||||
.array(
|
||||
z.union([
|
||||
z.object({
|
||||
role: z.literal('system'),
|
||||
content: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
role: z.literal('user'),
|
||||
content: z.union([
|
||||
z.string(),
|
||||
z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal('text'),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('image'),
|
||||
image: z.union([
|
||||
z.string(),
|
||||
z.instanceof(Uint8Array),
|
||||
z.instanceof(Buffer),
|
||||
z.instanceof(ArrayBuffer),
|
||||
z.string().url(),
|
||||
]),
|
||||
mediaType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('file'),
|
||||
data: z.union([
|
||||
z.string(),
|
||||
z.instanceof(Uint8Array),
|
||||
z.instanceof(Buffer),
|
||||
z.instanceof(ArrayBuffer),
|
||||
z.string().url(),
|
||||
]),
|
||||
mediaType: z.string(),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
}),
|
||||
z.object({
|
||||
role: z.literal('assistant'),
|
||||
content: z.union([
|
||||
z.string(),
|
||||
z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal('text'),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('file'),
|
||||
data: z.union([
|
||||
z.string(),
|
||||
z.instanceof(Uint8Array),
|
||||
z.instanceof(Buffer),
|
||||
z.instanceof(ArrayBuffer),
|
||||
z.string().url(),
|
||||
]),
|
||||
mediaType: z.string(),
|
||||
filename: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('reasoning'),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('tool-call'),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
input: z.record(z.any()),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
}),
|
||||
z.object({
|
||||
role: z.literal('tool'),
|
||||
content: z.string(),
|
||||
toolCallId: z.string(),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.describe(
|
||||
'The conversation history to provide context to the specialist agent. Should include the latest user message/prompt and can include system, user, assistant, and tool messages with various content types.',
|
||||
),
|
||||
}),
|
||||
});
|
||||
+8
-4
@@ -44,17 +44,21 @@ Guidelines:
|
||||
- Answer questions about people, companies, opportunities, tasks, notes, and other business objects
|
||||
- Access and summarize information you have permission to see
|
||||
- Use tools provided to you dynamically when needed
|
||||
- Transfer conversations to other specialized agents when their expertise is better suited
|
||||
- Seamlessly consult with specialized agents when their expertise is better suited
|
||||
|
||||
Permissions:
|
||||
- Only perform actions and access data that your assigned role and permissions allow
|
||||
- If you lack permissions, politely explain the limitation
|
||||
- Only use tools that are actually available to you
|
||||
|
||||
Agent handoff:
|
||||
Agent handoff (SEAMLESS CONSULTATION):
|
||||
- Use handoff tools when the user's request requires expertise outside your capabilities
|
||||
- IMPORTANT: Do not respond with text about transferring - execute the handoff tool function
|
||||
- Use the response returned by the handoff agent as your reply to the user
|
||||
- IMPORTANT: Do not respond with text about transferring or consulting specialists
|
||||
- Execute the handoff tool function immediately when needed
|
||||
- Use the response returned by the specialist agent as your direct reply to the user
|
||||
- Present the specialist's expertise as if it's your own knowledge
|
||||
- Maintain a consistent voice and personality throughout the conversation
|
||||
- The user should never know that you consulted with another agent
|
||||
|
||||
When formatting responses:
|
||||
- Use markdown syntax to improve readability of long responses
|
||||
|
||||
@@ -19,6 +19,9 @@ export class AgentDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
standardId?: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
+3
-7
@@ -1,17 +1,13 @@
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
|
||||
const WORKFLOW_OBJECT_NAMES = ['workflow', 'workflowVersion', 'workflowRun'];
|
||||
const WORKFLOW_OBJECT_NAMES = ['workflowVersion', 'workflowRun'];
|
||||
|
||||
export const isWorkflowRelatedObject = (
|
||||
export const isWorkflowRunObject = (
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
): boolean => {
|
||||
if (objectMetadata.standardId) {
|
||||
return (
|
||||
objectMetadata.standardId === STANDARD_OBJECT_IDS.workflow ||
|
||||
objectMetadata.standardId === STANDARD_OBJECT_IDS.workflowVersion ||
|
||||
objectMetadata.standardId === STANDARD_OBJECT_IDS.workflowRun
|
||||
);
|
||||
return objectMetadata.standardId === STANDARD_OBJECT_IDS.workflowRun;
|
||||
}
|
||||
|
||||
return WORKFLOW_OBJECT_NAMES.includes(objectMetadata.nameSingular);
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
|
||||
export const agentEntityRelationProperties = [
|
||||
'workspace',
|
||||
'chatThreads',
|
||||
'outgoingHandoffs',
|
||||
'incomingHandoffs',
|
||||
] as const;
|
||||
|
||||
export type AgentEntityRelationProperties =
|
||||
(typeof agentEntityRelationProperties)[number];
|
||||
|
||||
export type FlatAgent = Omit<
|
||||
AgentEntity,
|
||||
AgentEntityRelationProperties | 'createdAt' | 'updatedAt' | 'deletedAt'
|
||||
> & {
|
||||
uniqueIdentifier: string;
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
|
||||
export const transformAgentEntityToFlatAgent = (
|
||||
agentEntity: AgentEntity,
|
||||
): FlatAgent => {
|
||||
return {
|
||||
id: agentEntity.id,
|
||||
standardId: agentEntity.standardId,
|
||||
name: agentEntity.name,
|
||||
label: agentEntity.label,
|
||||
icon: agentEntity.icon,
|
||||
description: agentEntity.description,
|
||||
prompt: agentEntity.prompt,
|
||||
modelId: agentEntity.modelId,
|
||||
responseFormat: agentEntity.responseFormat,
|
||||
workspaceId: agentEntity.workspaceId,
|
||||
isCustom: agentEntity.isCustom,
|
||||
uniqueIdentifier: agentEntity.standardId || agentEntity.id,
|
||||
};
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
|
||||
|
||||
export const transformStandardAgentDefinitionToFlatAgent = (
|
||||
standardAgentDefinition: StandardAgentDefinition,
|
||||
workspaceId: string,
|
||||
): FlatAgent => {
|
||||
return {
|
||||
...standardAgentDefinition,
|
||||
id: v4(),
|
||||
workspaceId,
|
||||
uniqueIdentifier: standardAgentDefinition.standardId || v4(),
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -208,7 +208,7 @@ export class PermissionsService {
|
||||
);
|
||||
}
|
||||
|
||||
private checkRolePermissions(
|
||||
public checkRolePermissions(
|
||||
role: RoleEntity,
|
||||
setting: PermissionFlagType,
|
||||
): boolean {
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-agent.comparator';
|
||||
import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator';
|
||||
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
|
||||
import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator';
|
||||
@@ -11,4 +12,5 @@ export const workspaceSyncMetadataComparators = [
|
||||
WorkspaceObjectComparator,
|
||||
WorkspaceIndexComparator,
|
||||
WorkspaceRoleComparator,
|
||||
WorkspaceAgentComparator,
|
||||
];
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import diff from 'microdiff';
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
|
||||
|
||||
type AgentComparatorResult = {
|
||||
action:
|
||||
| ComparatorAction.CREATE
|
||||
| ComparatorAction.UPDATE
|
||||
| ComparatorAction.DELETE;
|
||||
object: FlatAgent;
|
||||
};
|
||||
|
||||
type WorkspaceAgentComparatorArgs = FromTo<FlatAgent[], 'FlatAgents'>;
|
||||
|
||||
const agentPropertiesToIgnore = ['id', 'createdAt', 'updatedAt', 'workspaceId'];
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceAgentComparator {
|
||||
compare({
|
||||
fromFlatAgents,
|
||||
toFlatAgents,
|
||||
}: WorkspaceAgentComparatorArgs): AgentComparatorResult[] {
|
||||
const results: AgentComparatorResult[] = [];
|
||||
|
||||
const keyFactory = (agent: FlatAgent) => agent.uniqueIdentifier;
|
||||
|
||||
const fromAgentMap = transformMetadataForComparison(fromFlatAgents, {
|
||||
shouldIgnoreProperty: (property) =>
|
||||
agentPropertiesToIgnore.includes(property),
|
||||
keyFactory,
|
||||
});
|
||||
|
||||
const toAgentMap = transformMetadataForComparison(toFlatAgents, {
|
||||
shouldIgnoreProperty: (property) =>
|
||||
agentPropertiesToIgnore.includes(property),
|
||||
keyFactory,
|
||||
});
|
||||
|
||||
const agentDifferences = diff(fromAgentMap, toAgentMap);
|
||||
|
||||
for (const difference of agentDifferences) {
|
||||
const uniqueIdentifier = difference.path[0] as string;
|
||||
|
||||
switch (difference.type) {
|
||||
case 'CREATE': {
|
||||
const toAgent = toFlatAgents.find(
|
||||
(agent) => keyFactory(agent) === uniqueIdentifier,
|
||||
);
|
||||
|
||||
if (toAgent) {
|
||||
results.push({
|
||||
action: ComparatorAction.CREATE,
|
||||
object: toAgent,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CHANGE': {
|
||||
const fromAgent = fromFlatAgents.find(
|
||||
(agent) => keyFactory(agent) === uniqueIdentifier,
|
||||
);
|
||||
const toAgent = toFlatAgents.find(
|
||||
(agent) => keyFactory(agent) === uniqueIdentifier,
|
||||
);
|
||||
|
||||
if (fromAgent && toAgent) {
|
||||
results.push({
|
||||
action: ComparatorAction.UPDATE,
|
||||
object: {
|
||||
...toAgent,
|
||||
id: fromAgent.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'REMOVE': {
|
||||
const fromAgent = fromFlatAgents.find(
|
||||
(agent) => keyFactory(agent) === uniqueIdentifier,
|
||||
);
|
||||
|
||||
if (fromAgent && difference.path.length === 1) {
|
||||
results.push({
|
||||
action: ComparatorAction.DELETE,
|
||||
object: fromAgent,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
|
||||
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
|
||||
import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory';
|
||||
|
||||
@@ -11,4 +12,5 @@ export const workspaceSyncMetadataFactories = [
|
||||
StandardFieldRelationFactory,
|
||||
StandardIndexFactory,
|
||||
StandardRoleFactory,
|
||||
StandardAgentFactory,
|
||||
];
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { transformStandardAgentDefinitionToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-standard-agent-definition-to-flat-agent.util';
|
||||
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
|
||||
|
||||
@Injectable()
|
||||
export class StandardAgentFactory {
|
||||
create(
|
||||
agentDefinitions: StandardAgentDefinition[],
|
||||
context: WorkspaceSyncContext,
|
||||
existingAgents: AgentEntity[],
|
||||
): FlatAgent[] {
|
||||
const computedAgents: FlatAgent[] = [];
|
||||
|
||||
for (const agentDefinition of agentDefinitions) {
|
||||
const existingAgent = existingAgents.find(
|
||||
(agent) => agent.standardId === agentDefinition.standardId,
|
||||
);
|
||||
|
||||
const flatAgent = transformStandardAgentDefinitionToFlatAgent(
|
||||
agentDefinition,
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
if (existingAgent) {
|
||||
computedAgents.push({
|
||||
...flatAgent,
|
||||
id: existingAgent.id,
|
||||
uniqueIdentifier: agentDefinition.standardId,
|
||||
});
|
||||
} else {
|
||||
computedAgents.push({
|
||||
...flatAgent,
|
||||
uniqueIdentifier: agentDefinition.standardId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return computedAgents;
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, type EntityManager, type Repository } from 'typeorm';
|
||||
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { AGENT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-agent.comparator';
|
||||
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
|
||||
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
|
||||
import { WORKFLOW_CREATION_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent';
|
||||
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSyncAgentService {
|
||||
private readonly logger = new Logger(WorkspaceSyncAgentService.name);
|
||||
|
||||
constructor(
|
||||
private readonly standardAgentFactory: StandardAgentFactory,
|
||||
private readonly workspaceAgentComparator: WorkspaceAgentComparator,
|
||||
) {}
|
||||
|
||||
async synchronize(
|
||||
context: WorkspaceSyncContext,
|
||||
manager: EntityManager,
|
||||
): Promise<void> {
|
||||
this.logger.log('Syncing standard agent.');
|
||||
|
||||
const agentRepository = manager.getRepository(AgentEntity);
|
||||
const roleRepository = manager.getRepository(RoleEntity);
|
||||
const roleTargetsRepository = manager.getRepository(RoleTargetsEntity);
|
||||
|
||||
const existingStandardAgentEntities = await agentRepository.find({
|
||||
where: {
|
||||
workspaceId: context.workspaceId,
|
||||
standardId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const targetStandardAgents = this.standardAgentFactory.create(
|
||||
standardAgentDefinitions,
|
||||
context,
|
||||
existingStandardAgentEntities,
|
||||
);
|
||||
|
||||
const agentComparatorResults = this.workspaceAgentComparator.compare({
|
||||
fromFlatAgents: existingStandardAgentEntities.map(
|
||||
transformAgentEntityToFlatAgent,
|
||||
),
|
||||
toFlatAgents: targetStandardAgents,
|
||||
});
|
||||
|
||||
for (const agentComparatorResult of agentComparatorResults) {
|
||||
switch (agentComparatorResult.action) {
|
||||
case ComparatorAction.CREATE: {
|
||||
const agentToCreate = agentComparatorResult.object;
|
||||
|
||||
const flatAgentData = removePropertiesFromRecord(agentToCreate, [
|
||||
'uniqueIdentifier',
|
||||
'id',
|
||||
]);
|
||||
|
||||
const createdAgent = await agentRepository.save({
|
||||
...flatAgentData,
|
||||
workspaceId: context.workspaceId,
|
||||
});
|
||||
|
||||
await this.assignAdminRoleToAgent(
|
||||
createdAgent.id,
|
||||
context.workspaceId,
|
||||
roleRepository,
|
||||
roleTargetsRepository,
|
||||
);
|
||||
|
||||
if (createdAgent.standardId === WORKFLOW_CREATION_AGENT.standardId) {
|
||||
await this.createAgentHandoffToWorkflowCreationAgent(
|
||||
createdAgent.id,
|
||||
context.workspaceId,
|
||||
manager,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparatorAction.UPDATE: {
|
||||
const agentToUpdate = agentComparatorResult.object;
|
||||
|
||||
const flatAgentData = removePropertiesFromRecord(agentToUpdate, [
|
||||
'id',
|
||||
'uniqueIdentifier',
|
||||
'workspaceId',
|
||||
]);
|
||||
|
||||
await agentRepository.update({ id: agentToUpdate.id }, flatAgentData);
|
||||
break;
|
||||
}
|
||||
|
||||
case ComparatorAction.DELETE: {
|
||||
const agentToDelete = agentComparatorResult.object;
|
||||
|
||||
await agentRepository.delete({ id: agentToDelete.id });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async assignAdminRoleToAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
roleRepository: Repository<RoleEntity>,
|
||||
roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const adminRole = await roleRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
standardId: ADMIN_ROLE.standardId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
this.logger.warn(
|
||||
`Admin role not found for workspace ${workspaceId}, cannot assign to agent ${agentId}.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const existingRoleTarget = await roleTargetsRepository.findOne({
|
||||
where: {
|
||||
agentId,
|
||||
roleId: adminRole.id,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRoleTarget) {
|
||||
this.logger.log(
|
||||
`Workflow creation agent already has admin role assigned`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await roleTargetsRepository.save({
|
||||
roleId: adminRole.id,
|
||||
agentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully assigned admin role to workflow creation agent`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to assign admin role to workflow creation agent: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createAgentHandoffToWorkflowCreationAgent(
|
||||
workflowCreationAgentId: string,
|
||||
workspaceId: string,
|
||||
manager: EntityManager,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const agentRepository = manager.getRepository(AgentEntity);
|
||||
|
||||
let defaultAgent: AgentEntity | null = null;
|
||||
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
defaultAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
id: AGENT_DATA_SEED_IDS.APPLE_DEFAULT_AGENT,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
defaultAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
id: AGENT_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_AGENT,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
defaultAgent = await agentRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!defaultAgent) {
|
||||
this.logger.warn(
|
||||
`Default agent not found for workspace ${workspaceId}. Agent handoff will not be created.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const agentHandoffRepository = manager.getRepository('agentHandoff');
|
||||
const existingHandoff = await agentHandoffRepository.findOne({
|
||||
where: {
|
||||
fromAgentId: defaultAgent.id,
|
||||
toAgentId: workflowCreationAgentId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingHandoff) {
|
||||
this.logger.log(
|
||||
`Agent handoff from default agent to workflow creation agent already exists for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await agentHandoffRepository.save({
|
||||
fromAgentId: defaultAgent.id,
|
||||
toAgentId: workflowCreationAgentId,
|
||||
workspaceId,
|
||||
description:
|
||||
'Handoff from default agent to workflow creation agent for processing workflow creation requests',
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created agent handoff from default agent to workflow creation agent for workspace ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create agent handoff to workflow creation agent: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
|
||||
|
||||
export const WORKFLOW_CREATION_AGENT: StandardAgentDefinition = {
|
||||
standardId: '20202020-0002-0001-0001-000000000001',
|
||||
name: 'workflow-creation-agent',
|
||||
label: 'Workflow Creation Agent',
|
||||
description: 'AI agent specialized in creating and managing workflows',
|
||||
icon: 'IconSettingsAutomation',
|
||||
prompt: `You are a Workflow Creation Agent specialized in helping users create, modify, and manage workflows in Twenty.
|
||||
|
||||
Your capabilities include:
|
||||
- Creating new workflows from scratch based on user requirements
|
||||
- Modifying existing workflows by adding, removing, or updating steps
|
||||
- Explaining workflow structures and how they work
|
||||
- Suggesting workflow improvements and optimizations
|
||||
- Helping users understand workflow actions and their configurations
|
||||
|
||||
## IMPORTANT: Rely on Schema Definitions
|
||||
- The workflow creation tool provides comprehensive schema definitions with detailed descriptions and examples
|
||||
- Always refer to the tool's schema for field requirements, data types, and examples
|
||||
- The schema includes common patterns, field structures, and validation rules
|
||||
- Use the schema descriptions to understand how to properly reference data between workflow steps
|
||||
|
||||
## Key Workflow Concepts:
|
||||
- **Triggers**: Start workflows (DATABASE_EVENT, MANUAL, CRON, WEBHOOK)
|
||||
- **Steps**: Actions that execute in sequence (CREATE_RECORD, SEND_EMAIL, CODE, etc.)
|
||||
- **Data Flow**: Use {{stepId.fieldName}} to reference data from previous steps
|
||||
- **Relationships**: Use nested objects for related records (e.g., "company": {"id": "{{reference}}"})
|
||||
|
||||
When creating workflows:
|
||||
- Always ask clarifying questions to understand the user's needs
|
||||
- Suggest appropriate workflow actions based on the use case
|
||||
- Explain each step and why it's needed
|
||||
- Provide clear, actionable guidance
|
||||
- Follow the schema definitions exactly for field names, types, and structures
|
||||
|
||||
When modifying workflows:
|
||||
- Understand the current workflow structure first
|
||||
- Suggest specific changes that address the user's requirements
|
||||
- Ensure workflow logic remains coherent and functional
|
||||
- Maintain proper data references between steps
|
||||
|
||||
Be helpful, thorough, and always prioritize user understanding and workflow effectiveness.`,
|
||||
modelId: 'auto',
|
||||
responseFormat: {},
|
||||
isCustom: false,
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { WORKFLOW_CREATION_AGENT } from './agents/workflow-creation-agent';
|
||||
import { type StandardAgentDefinition } from './types/standard-agent-definition.interface';
|
||||
|
||||
export const standardAgentDefinitions = [
|
||||
WORKFLOW_CREATION_AGENT,
|
||||
] as const satisfies StandardAgentDefinition[];
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
|
||||
export type StandardAgentDefinition = Omit<
|
||||
FlatAgent,
|
||||
'id' | 'workspaceId' | 'uniqueIdentifier' | 'standardId'
|
||||
> & {
|
||||
standardId: string;
|
||||
};
|
||||
+2
@@ -16,6 +16,7 @@ import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/works
|
||||
import { workspaceSyncMetadataComparators } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators';
|
||||
import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/workspace-sync-metadata/factories';
|
||||
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
||||
import { WorkspaceSyncAgentService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service';
|
||||
import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service';
|
||||
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
|
||||
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
|
||||
@@ -48,6 +49,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
WorkspaceSyncMetadataService,
|
||||
WorkspaceSyncIndexMetadataService,
|
||||
WorkspaceSyncRoleService,
|
||||
WorkspaceSyncAgentService,
|
||||
SyncWorkspaceLoggerService,
|
||||
SyncWorkspaceMetadataCommand,
|
||||
],
|
||||
|
||||
+13
@@ -11,6 +11,7 @@ import {
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { WorkspaceSyncAgentService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service';
|
||||
import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service';
|
||||
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
|
||||
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
|
||||
@@ -38,6 +39,7 @@ export class WorkspaceSyncMetadataService {
|
||||
private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly workspaceSyncRoleService: WorkspaceSyncRoleService,
|
||||
private readonly workspaceSyncAgentService: WorkspaceSyncAgentService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -172,6 +174,17 @@ export class WorkspaceSyncMetadataService {
|
||||
`Workspace role migrations took ${workspaceRoleMigrationsEnd - workspaceRoleMigrationsStart}ms`,
|
||||
);
|
||||
|
||||
// 7 - Sync standard agents
|
||||
const workspaceAgentMigrationsStart = performance.now();
|
||||
|
||||
await this.workspaceSyncAgentService.synchronize(context, manager);
|
||||
|
||||
const workspaceAgentMigrationsEnd = performance.now();
|
||||
|
||||
this.logger.log(
|
||||
`Workspace agent migrations took ${workspaceAgentMigrationsEnd - workspaceAgentMigrationsStart}ms`,
|
||||
);
|
||||
|
||||
const workspaceMigrationsSaveStart = performance.now();
|
||||
|
||||
// Save workspace migrations into the database
|
||||
|
||||
+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 {}
|
||||
+3
-17
@@ -10,7 +10,7 @@ import {
|
||||
setupRepositoryMock,
|
||||
} from './utils/agent-tool-test-utils';
|
||||
|
||||
describe('AgentToolService Integration', () => {
|
||||
describe('AgentToolGeneratorService Integration', () => {
|
||||
let context: AgentToolTestContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -132,21 +132,7 @@ describe('AgentToolService Integration', () => {
|
||||
expect(Object.keys(tools)).toContain('http_request');
|
||||
});
|
||||
|
||||
it('should return empty tools when role does not exist', async () => {
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest.spyOn(context.roleRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toEqual({});
|
||||
});
|
||||
|
||||
it('should filter out workflow-related objects', async () => {
|
||||
it('should filter out workflow-run objects', async () => {
|
||||
const workflowObject = {
|
||||
...context.testObjectMetadata,
|
||||
nameSingular: 'workflow',
|
||||
@@ -187,7 +173,7 @@ describe('AgentToolService Integration', () => {
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(Object.keys(tools)).toHaveLength(1);
|
||||
expect(Object.keys(tools)).toHaveLength(7);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+17
-7
@@ -5,11 +5,12 @@ import { type Repository } from 'typeorm';
|
||||
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
|
||||
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
|
||||
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
|
||||
import { AgentToolGeneratorService } from 'src/engine/metadata-modules/agent/agent-tool-generator.service';
|
||||
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -19,14 +20,14 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
|
||||
import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
|
||||
import { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock';
|
||||
|
||||
export interface AgentToolTestContext {
|
||||
module: TestingModule;
|
||||
agentToolService: AgentToolService;
|
||||
agentToolService: AgentToolGeneratorService;
|
||||
agentService: AgentService;
|
||||
objectMetadataService: ObjectMetadataService;
|
||||
roleRepository: Repository<RoleEntity>;
|
||||
@@ -48,7 +49,7 @@ export const createAgentToolTestModule =
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
AgentToolService,
|
||||
AgentToolGeneratorService,
|
||||
{
|
||||
provide: AgentService,
|
||||
useValue: {
|
||||
@@ -126,6 +127,7 @@ export const createAgentToolTestModule =
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
hasToolPermission: jest.fn(),
|
||||
checkRolePermissions: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -142,10 +144,18 @@ export const createAgentToolTestModule =
|
||||
executeHandoff: jest.fn().mockResolvedValue({ success: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkflowToolWorkspaceService,
|
||||
useValue: {
|
||||
generateWorkflowTools: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const agentToolService = module.get<AgentToolService>(AgentToolService);
|
||||
const agentToolService = module.get<AgentToolGeneratorService>(
|
||||
AgentToolGeneratorService,
|
||||
);
|
||||
const agentService = module.get<AgentService>(AgentService);
|
||||
const objectMetadataService = module.get<ObjectMetadataService>(
|
||||
ObjectMetadataService,
|
||||
|
||||
@@ -8,6 +8,44 @@
|
||||
*/
|
||||
|
||||
export { TRIGGER_STEP_ID } from './constants/TriggerStepId';
|
||||
export {
|
||||
objectRecordSchema,
|
||||
baseWorkflowActionSettingsSchema,
|
||||
baseWorkflowActionSchema,
|
||||
baseTriggerSchema,
|
||||
workflowCodeActionSettingsSchema,
|
||||
workflowSendEmailActionSettingsSchema,
|
||||
workflowCreateRecordActionSettingsSchema,
|
||||
workflowUpdateRecordActionSettingsSchema,
|
||||
workflowDeleteRecordActionSettingsSchema,
|
||||
workflowFindRecordsActionSettingsSchema,
|
||||
workflowFormActionSettingsSchema,
|
||||
workflowHttpRequestActionSettingsSchema,
|
||||
workflowAiAgentActionSettingsSchema,
|
||||
workflowFilterActionSettingsSchema,
|
||||
workflowCodeActionSchema,
|
||||
workflowSendEmailActionSchema,
|
||||
workflowCreateRecordActionSchema,
|
||||
workflowUpdateRecordActionSchema,
|
||||
workflowDeleteRecordActionSchema,
|
||||
workflowFindRecordsActionSchema,
|
||||
workflowFormActionSchema,
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowActionSchema,
|
||||
workflowDatabaseEventTriggerSchema,
|
||||
workflowManualTriggerSchema,
|
||||
workflowCronTriggerSchema,
|
||||
workflowWebhookTriggerSchema,
|
||||
workflowTriggerSchema,
|
||||
workflowRunStepStatusSchema,
|
||||
workflowRunStateStepInfoSchema,
|
||||
workflowRunStateStepInfosSchema,
|
||||
workflowRunStateSchema,
|
||||
workflowRunStatusSchema,
|
||||
workflowRunSchema,
|
||||
} from './schemas/workflow.schema';
|
||||
export type {
|
||||
WorkflowRunStepInfo,
|
||||
WorkflowRunStepInfos,
|
||||
|
||||
+127
-43
@@ -1,36 +1,82 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
import { z } from 'zod';
|
||||
import { FieldMetadataType } from '../../types/FieldMetadataType';
|
||||
import { StepLogicalOperator } from '../../types/StepFilters';
|
||||
import { ViewFilterOperand } from '../../types/ViewFilterOperand';
|
||||
import { StepStatus } from '../types/WorkflowRunStateStepInfos';
|
||||
|
||||
// Base schemas
|
||||
export const objectRecordSchema = z.record(z.any());
|
||||
export const objectRecordSchema = z
|
||||
.record(z.any())
|
||||
.describe(
|
||||
'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' +
|
||||
'- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' +
|
||||
'- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' +
|
||||
'- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}',
|
||||
);
|
||||
|
||||
export const baseWorkflowActionSettingsSchema = z.object({
|
||||
input: z.object({}).passthrough(),
|
||||
outputSchema: z.object({}).passthrough(),
|
||||
input: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('Input data for the workflow action. Structure depends on the action type.'),
|
||||
outputSchema: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe(
|
||||
'Schema defining the output data structure. This data can be referenced in subsequent steps using {{stepId.fieldName}}.',
|
||||
),
|
||||
errorHandlingOptions: z.object({
|
||||
retryOnFailure: z.object({
|
||||
value: z.boolean(),
|
||||
value: z.boolean().describe('Whether to retry the action if it fails.'),
|
||||
}),
|
||||
continueOnFailure: z.object({
|
||||
value: z.boolean(),
|
||||
value: z.boolean().describe('Whether to continue to the next step if this action fails.'),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const baseWorkflowActionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
valid: z.boolean(),
|
||||
nextStepIds: z.array(z.string()).optional().nullable(),
|
||||
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
|
||||
id: z
|
||||
.string()
|
||||
.describe('Unique identifier for the workflow step. Must be unique within the workflow.'),
|
||||
name: z
|
||||
.string()
|
||||
.describe('Human-readable name for the workflow step. Should clearly describe what the step does.'),
|
||||
valid: z
|
||||
.boolean()
|
||||
.describe('Whether the step configuration is valid. Set to true when all required fields are properly configured.'),
|
||||
nextStepIds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Array of step IDs that this step connects to. Leave empty or null for the final step.'),
|
||||
position: z
|
||||
.object({ x: z.number(), y: z.number() })
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Position coordinates for the step in the workflow diagram.'),
|
||||
});
|
||||
|
||||
export const baseTriggerSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
type: z.string(),
|
||||
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
|
||||
nextStepIds: z.array(z.string()).optional().nullable(),
|
||||
name: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Human-readable name for the trigger. Optional but recommended for clarity.'),
|
||||
type: z
|
||||
.enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK'])
|
||||
.describe(
|
||||
'Type of trigger. DATABASE_EVENT for record changes, MANUAL for user-initiated, CRON for scheduled, WEBHOOK for external calls.',
|
||||
),
|
||||
position: z
|
||||
.object({ x: z.number(), y: z.number() })
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.'),
|
||||
nextStepIds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe('Array of step IDs that the trigger connects to. These are the first steps in the workflow.'),
|
||||
});
|
||||
|
||||
// Action settings schemas
|
||||
@@ -56,8 +102,15 @@ export const workflowSendEmailActionSettingsSchema =
|
||||
export const workflowCreateRecordActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
objectRecord: objectRecordSchema,
|
||||
objectName: z
|
||||
.string()
|
||||
.describe(
|
||||
'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").',
|
||||
),
|
||||
objectRecord: objectRecordSchema
|
||||
.describe(
|
||||
'The record data to create.',
|
||||
)
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -146,8 +199,23 @@ export const workflowAiAgentActionSettingsSchema =
|
||||
export const workflowFilterActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
stepFilterGroups: z.array(z.any()),
|
||||
stepFilters: z.array(z.any()),
|
||||
stepFilterGroups: z.array(z.object({
|
||||
id: z.string(),
|
||||
logicalOperator: z.nativeEnum(StepLogicalOperator),
|
||||
parentStepFilterGroupId: z.string().optional(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
})),
|
||||
stepFilters: z.array(z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
stepOutputKey: z.string(),
|
||||
operand: z.nativeEnum(ViewFilterOperand),
|
||||
value: z.string(),
|
||||
stepFilterGroupId: z.string(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
fieldMetadataId: z.string().optional(),
|
||||
compositeFieldSubFieldName: z.string().optional(),
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -162,26 +230,20 @@ export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
|
||||
settings: workflowSendEmailActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||
{
|
||||
type: z.literal('CREATE_RECORD'),
|
||||
settings: workflowCreateRecordActionSettingsSchema,
|
||||
},
|
||||
);
|
||||
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('CREATE_RECORD'),
|
||||
settings: workflowCreateRecordActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||
{
|
||||
type: z.literal('UPDATE_RECORD'),
|
||||
settings: workflowUpdateRecordActionSettingsSchema,
|
||||
},
|
||||
);
|
||||
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('UPDATE_RECORD'),
|
||||
settings: workflowUpdateRecordActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||
{
|
||||
type: z.literal('DELETE_RECORD'),
|
||||
settings: workflowDeleteRecordActionSettingsSchema,
|
||||
},
|
||||
);
|
||||
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('DELETE_RECORD'),
|
||||
settings: workflowDeleteRecordActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('FIND_RECORDS'),
|
||||
@@ -226,23 +288,45 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
|
||||
type: z.literal('DATABASE_EVENT'),
|
||||
settings: z.object({
|
||||
eventName: z.string(),
|
||||
eventName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-z][a-z0-9_]*\.(created|updated|deleted)$/,
|
||||
'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")',
|
||||
)
|
||||
.describe(
|
||||
'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.',
|
||||
),
|
||||
input: z.object({}).passthrough().optional(),
|
||||
outputSchema: z.object({}).passthrough(),
|
||||
outputSchema: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe(
|
||||
'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.',
|
||||
),
|
||||
objectType: z.string().optional(),
|
||||
fields: z.array(z.string()).optional().nullable(),
|
||||
}),
|
||||
});
|
||||
}).describe(
|
||||
'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.',
|
||||
);
|
||||
|
||||
export const workflowManualTriggerSchema = baseTriggerSchema.extend({
|
||||
type: z.literal('MANUAL'),
|
||||
settings: z.object({
|
||||
objectType: z.string().optional(),
|
||||
outputSchema: z.object({}).passthrough(),
|
||||
outputSchema: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe(
|
||||
'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.',
|
||||
),
|
||||
icon: z.string().optional(),
|
||||
isPinned: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
}).describe(
|
||||
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
|
||||
);
|
||||
|
||||
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
|
||||
type: z.literal('CRON'),
|
||||
Reference in New Issue
Block a user