AI SDK v5 migration (#14549)
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+36
-10
@@ -1,8 +1,12 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
|
||||
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { AiService } from 'src/engine/core-modules/ai/services/ai.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||
|
||||
import { AiController } from './ai.controller';
|
||||
|
||||
@@ -11,6 +15,7 @@ describe('AiController', () => {
|
||||
let aiService: jest.Mocked<AiService>;
|
||||
let featureFlagService: jest.Mocked<FeatureFlagService>;
|
||||
let aiBillingService: jest.Mocked<AIBillingService>;
|
||||
let aiModelRegistryService: jest.Mocked<AiModelRegistryService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockAiService = {
|
||||
@@ -26,6 +31,14 @@ describe('AiController', () => {
|
||||
calculateAndBillUsage: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAiModelRegistryService = {
|
||||
getDefaultModel: jest.fn().mockReturnValue({
|
||||
modelId: 'gpt-4o',
|
||||
provider: ModelProvider.OPENAI,
|
||||
model: openai('gpt-4o'),
|
||||
}),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AiController],
|
||||
providers: [
|
||||
@@ -41,6 +54,10 @@ describe('AiController', () => {
|
||||
provide: AIBillingService,
|
||||
useValue: mockAIBillingService,
|
||||
},
|
||||
{
|
||||
provide: AiModelRegistryService,
|
||||
useValue: mockAiModelRegistryService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -48,6 +65,7 @@ describe('AiController', () => {
|
||||
aiService = module.get(AiService);
|
||||
featureFlagService = module.get(FeatureFlagService);
|
||||
aiBillingService = module.get(AIBillingService);
|
||||
aiModelRegistryService = module.get(AiModelRegistryService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@@ -61,7 +79,7 @@ describe('AiController', () => {
|
||||
const mockRequest = {
|
||||
messages: [{ role: 'user' as const, content: 'Hello' }],
|
||||
temperature: 0.7,
|
||||
maxTokens: 100,
|
||||
maxOutputTokens: 100,
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
@@ -70,19 +88,23 @@ describe('AiController', () => {
|
||||
end: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const mockModel = { modelId: 'gpt-4o' } as any;
|
||||
const mockModel = openai('gpt-4o');
|
||||
|
||||
aiService.getModel.mockReturnValue(mockModel);
|
||||
aiModelRegistryService.getDefaultModel.mockReturnValue({
|
||||
modelId: 'gpt-4o',
|
||||
provider: ModelProvider.OPENAI,
|
||||
model: mockModel,
|
||||
});
|
||||
|
||||
const mockUsage = {
|
||||
promptTokens: 10,
|
||||
completionTokens: 20,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
totalTokens: 30,
|
||||
};
|
||||
|
||||
const mockStreamTextResult = {
|
||||
usage: Promise.resolve(mockUsage),
|
||||
pipeDataStreamToResponse: jest.fn(),
|
||||
pipeUIMessageStreamToResponse: jest.fn(),
|
||||
};
|
||||
|
||||
aiService.streamText.mockReturnValue(mockStreamTextResult as any);
|
||||
@@ -96,12 +118,12 @@ describe('AiController', () => {
|
||||
messages: mockRequest.messages,
|
||||
options: {
|
||||
temperature: 0.7,
|
||||
maxTokens: 100,
|
||||
maxOutputTokens: 100,
|
||||
model: mockModel,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
mockStreamTextResult.pipeDataStreamToResponse,
|
||||
mockStreamTextResult.pipeUIMessageStreamToResponse,
|
||||
).toHaveBeenCalledWith(mockRes);
|
||||
expect(aiBillingService.calculateAndBillUsage).toHaveBeenCalledWith(
|
||||
mockModel.modelId,
|
||||
@@ -131,7 +153,11 @@ describe('AiController', () => {
|
||||
|
||||
const mockRes = {} as any;
|
||||
|
||||
aiService.getModel.mockReturnValue({ modelId: 'gpt-4o' } as any);
|
||||
aiModelRegistryService.getDefaultModel.mockReturnValue({
|
||||
modelId: 'gpt-4o',
|
||||
provider: ModelProvider.OPENAI,
|
||||
model: openai('gpt-4o'),
|
||||
});
|
||||
aiService.streamText.mockImplementation(() => {
|
||||
throw new Error('Service error');
|
||||
});
|
||||
|
||||
@@ -8,21 +8,22 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type CoreMessage } from 'ai';
|
||||
import { type ModelMessage } from 'ai';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { AiService } from 'src/engine/core-modules/ai/services/ai.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||
|
||||
export interface ChatRequest {
|
||||
messages: CoreMessage[];
|
||||
messages: ModelMessage[];
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
maxOutputTokens?: number;
|
||||
}
|
||||
|
||||
@Controller('chat')
|
||||
@@ -32,6 +33,7 @@ export class AiController {
|
||||
private readonly aiService: AiService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@@ -52,7 +54,7 @@ export class AiController {
|
||||
);
|
||||
}
|
||||
|
||||
const { messages, temperature, maxTokens } = request;
|
||||
const { messages, temperature, maxOutputTokens } = request;
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
throw new HttpException(
|
||||
@@ -62,27 +64,26 @@ export class AiController {
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Add support for custom models
|
||||
const model = this.aiService.getModel(undefined);
|
||||
const registeredModel = this.aiModelRegistryService.getDefaultModel();
|
||||
|
||||
const result = this.aiService.streamText({
|
||||
messages,
|
||||
options: {
|
||||
temperature,
|
||||
maxTokens,
|
||||
model,
|
||||
maxOutputTokens,
|
||||
model: registeredModel.model,
|
||||
},
|
||||
});
|
||||
|
||||
result.usage.then((usage) => {
|
||||
this.aiBillingService.calculateAndBillUsage(
|
||||
model.modelId,
|
||||
registeredModel.modelId,
|
||||
usage,
|
||||
workspace.id,
|
||||
);
|
||||
});
|
||||
|
||||
result.pipeDataStreamToResponse(res);
|
||||
result.pipeUIMessageStreamToResponse(res);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
+4
-4
@@ -11,8 +11,8 @@ describe('AIBillingService', () => {
|
||||
let mockWorkspaceEventEmitter: jest.Mocked<WorkspaceEventEmitter>;
|
||||
|
||||
const mockTokenUsage = {
|
||||
promptTokens: 1000,
|
||||
completionTokens: 500,
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
};
|
||||
|
||||
@@ -63,8 +63,8 @@ describe('AIBillingService', () => {
|
||||
|
||||
it('should calculate cost correctly with different token usage', async () => {
|
||||
const differentTokenUsage = {
|
||||
promptTokens: 2000,
|
||||
completionTokens: 1000,
|
||||
inputTokens: 2000,
|
||||
outputTokens: 1000,
|
||||
totalTokens: 3000,
|
||||
};
|
||||
|
||||
|
||||
+12
-10
@@ -1,17 +1,19 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { jsonSchema } from 'ai';
|
||||
|
||||
import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const';
|
||||
import { type JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
|
||||
import { McpService } from 'src/engine/core-modules/ai/services/mcp.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
|
||||
import { MCP_SERVER_METADATA } from 'src/engine/core-modules/ai/constants/mcp.const';
|
||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { McpService } from 'src/engine/core-modules/ai/services/mcp.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
describe('McpService', () => {
|
||||
let service: McpService;
|
||||
@@ -197,7 +199,7 @@ describe('McpService', () => {
|
||||
|
||||
const mockTool = {
|
||||
description: 'Test tool',
|
||||
parameters: { jsonSchema: { type: 'object', properties: {} } },
|
||||
inputSchema: jsonSchema({ type: 'object', properties: {} }),
|
||||
execute: jest.fn().mockResolvedValue({ result: 'success' }),
|
||||
};
|
||||
|
||||
@@ -245,7 +247,7 @@ describe('McpService', () => {
|
||||
|
||||
const mockTool = {
|
||||
description: 'Test tool',
|
||||
parameters: { jsonSchema: { type: 'object', properties: {} } },
|
||||
inputSchema: jsonSchema({ type: 'object', properties: {} }),
|
||||
execute: jest.fn().mockResolvedValue({ result: 'success' }),
|
||||
};
|
||||
|
||||
@@ -299,7 +301,7 @@ describe('McpService', () => {
|
||||
const mockToolsMap = {
|
||||
testTool: {
|
||||
description: 'Test tool',
|
||||
parameters: { jsonSchema: { type: 'object', properties: {} } },
|
||||
inputSchema: jsonSchema({ type: 'object', properties: {} }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -328,7 +330,7 @@ describe('McpService', () => {
|
||||
{
|
||||
name: 'testTool',
|
||||
description: 'Test tool',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
inputSchema: jsonSchema({ type: 'object', properties: {} }),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
+4
-2
@@ -1,5 +1,7 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { jsonSchema } from 'ai';
|
||||
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
|
||||
@@ -33,7 +35,7 @@ describe('ToolAdapterService', () => {
|
||||
}));
|
||||
const unflaggedTool: Tool = {
|
||||
description: 'HTTP Request tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
inputSchema: jsonSchema({ type: 'object', properties: {} }),
|
||||
execute: unflaggedToolExecute,
|
||||
};
|
||||
|
||||
@@ -44,7 +46,7 @@ describe('ToolAdapterService', () => {
|
||||
}));
|
||||
const flaggedTool: Tool = {
|
||||
description: 'Send Email tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
inputSchema: jsonSchema({ type: 'object', properties: {} }),
|
||||
execute: flaggedToolExecute,
|
||||
flag: PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { LanguageModelUsage } from 'ai';
|
||||
|
||||
import { type ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier';
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
@@ -8,12 +10,6 @@ import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/bil
|
||||
import { type BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
|
||||
export interface TokenUsage {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AIBillingService {
|
||||
private readonly logger = new Logger(AIBillingService.name);
|
||||
@@ -23,7 +19,10 @@ export class AIBillingService {
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
) {}
|
||||
|
||||
async calculateCost(modelId: ModelId, usage: TokenUsage): Promise<number> {
|
||||
async calculateCost(
|
||||
modelId: ModelId,
|
||||
usage: LanguageModelUsage,
|
||||
): Promise<number> {
|
||||
const model = this.aiModelRegistryService.getEffectiveModelConfig(modelId);
|
||||
|
||||
if (!model) {
|
||||
@@ -31,9 +30,9 @@ export class AIBillingService {
|
||||
}
|
||||
|
||||
const inputCost =
|
||||
(usage.promptTokens / 1000) * model.inputCostPer1kTokensInCents;
|
||||
((usage.inputTokens ?? 0) / 1000) * model.inputCostPer1kTokensInCents;
|
||||
const outputCost =
|
||||
(usage.completionTokens / 1000) * model.outputCostPer1kTokensInCents;
|
||||
((usage.outputTokens ?? 0) / 1000) * model.outputCostPer1kTokensInCents;
|
||||
|
||||
const totalCost = inputCost + outputCost;
|
||||
|
||||
@@ -46,7 +45,7 @@ export class AIBillingService {
|
||||
|
||||
async calculateAndBillUsage(
|
||||
modelId: ModelId,
|
||||
usage: TokenUsage,
|
||||
usage: LanguageModelUsage,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const costInCents = await this.calculateCost(modelId, usage);
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ export class AiModelRegistryService {
|
||||
return Array.from(this.modelRegistry.values());
|
||||
}
|
||||
|
||||
getDefaultModel(): RegisteredAIModel | undefined {
|
||||
getDefaultModel(): RegisteredAIModel {
|
||||
const defaultModelId = this.twentyConfigService.get('DEFAULT_MODEL_ID');
|
||||
let model = this.getModel(defaultModelId);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type CoreMessage, streamText, LanguageModelV1 } from 'ai';
|
||||
import { LanguageModel, type ModelMessage, streamText } from 'ai';
|
||||
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
|
||||
@@ -28,18 +28,18 @@ export class AiService {
|
||||
messages,
|
||||
options,
|
||||
}: {
|
||||
messages: CoreMessage[];
|
||||
messages: ModelMessage[];
|
||||
options: {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
model: LanguageModelV1;
|
||||
maxOutputTokens?: number;
|
||||
model: LanguageModel;
|
||||
};
|
||||
}) {
|
||||
return streamText({
|
||||
model: options.model,
|
||||
messages,
|
||||
temperature: options?.temperature,
|
||||
maxTokens: options?.maxTokens,
|
||||
maxOutputTokens: options?.maxOutputTokens,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +204,11 @@ export class McpService {
|
||||
|
||||
private handleToolsListing(id: string | number, toolSet: ToolSet) {
|
||||
const toolsArray = Object.entries(toolSet)
|
||||
.filter(([, def]) => !!def.parameters.jsonSchema)
|
||||
.filter(([, def]) => !!def.inputSchema)
|
||||
.map(([name, def]) => ({
|
||||
name,
|
||||
description: def.description,
|
||||
inputSchema: def.parameters.jsonSchema,
|
||||
inputSchema: def.inputSchema,
|
||||
}));
|
||||
|
||||
return wrapJsonRpcResponse(id, {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ToolAdapterService {
|
||||
private createToolSet(tool: Tool) {
|
||||
return {
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
inputSchema: tool.inputSchema,
|
||||
execute: async (parameters: { input: ToolInput }) =>
|
||||
tool.execute(parameters.input),
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ export class ToolService {
|
||||
if (objectPermission.canUpdate) {
|
||||
tools[`create_${objectMetadata.nameSingular}`] = {
|
||||
description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`,
|
||||
parameters: getRecordInputSchema(objectMetadata),
|
||||
inputSchema: getRecordInputSchema(objectMetadata),
|
||||
execute: async (parameters) => {
|
||||
return this.createRecord(
|
||||
objectMetadata.nameSingular,
|
||||
@@ -74,7 +74,7 @@ export class ToolService {
|
||||
|
||||
tools[`update_${objectMetadata.nameSingular}`] = {
|
||||
description: `Update an existing ${objectMetadata.labelSingular} record. Provide the record ID and only the fields you want to change. Unspecified fields will remain unchanged. Returns the updated record with all current data.`,
|
||||
parameters: getRecordInputSchema(objectMetadata),
|
||||
inputSchema: getRecordInputSchema(objectMetadata),
|
||||
execute: async (parameters) => {
|
||||
return this.updateRecord(
|
||||
objectMetadata.nameSingular,
|
||||
@@ -89,7 +89,7 @@ export class ToolService {
|
||||
if (objectPermission.canRead) {
|
||||
tools[`find_${objectMetadata.nameSingular}`] = {
|
||||
description: `Search for ${objectMetadata.labelSingular} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination. Returns an array of matching records with their full data.`,
|
||||
parameters: generateFindToolSchema(objectMetadata),
|
||||
inputSchema: generateFindToolSchema(objectMetadata),
|
||||
execute: async (parameters) => {
|
||||
return this.findRecords(
|
||||
objectMetadata.nameSingular,
|
||||
@@ -102,7 +102,7 @@ export class ToolService {
|
||||
|
||||
tools[`find_one_${objectMetadata.nameSingular}`] = {
|
||||
description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`,
|
||||
parameters: generateFindOneToolSchema(),
|
||||
inputSchema: generateFindOneToolSchema(),
|
||||
execute: async (parameters) => {
|
||||
return this.findOneRecord(
|
||||
objectMetadata.nameSingular,
|
||||
@@ -117,7 +117,7 @@ export class ToolService {
|
||||
if (objectPermission.canSoftDelete) {
|
||||
tools[`soft_delete_${objectMetadata.nameSingular}`] = {
|
||||
description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`,
|
||||
parameters: generateSoftDeleteToolSchema(),
|
||||
inputSchema: generateSoftDeleteToolSchema(),
|
||||
execute: async (parameters) => {
|
||||
return this.softDeleteRecord(
|
||||
objectMetadata.nameSingular,
|
||||
@@ -130,7 +130,7 @@ export class ToolService {
|
||||
|
||||
tools[`soft_delete_many_${objectMetadata.nameSingular}`] = {
|
||||
description: `Soft delete multiple ${objectMetadata.labelSingular} records at once by providing an array of record IDs. All records are marked as deleted but remain in the database. This is efficient for bulk operations and preserves all data.`,
|
||||
parameters: generateBulkDeleteToolSchema(),
|
||||
inputSchema: generateBulkDeleteToolSchema(),
|
||||
execute: async (parameters) => {
|
||||
return this.softDeleteManyRecords(
|
||||
objectMetadata.nameSingular,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { AuditContextMock } from 'test/utils/audit-context.mock';
|
||||
|
||||
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
|
||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
|
||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -135,7 +136,7 @@ describe('AuditService', () => {
|
||||
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||
|
||||
const result = await context.createObjectEvent(
|
||||
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||
OBJECT_RECORD_CREATED_EVENT,
|
||||
{
|
||||
recordId: 'test-record-id',
|
||||
objectMetadataId: 'test-object-metadata-id',
|
||||
|
||||
+6
-8
@@ -1,10 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const baseEventSchema = z
|
||||
.object({
|
||||
timestamp: z.string(),
|
||||
userId: z.string().nullish(),
|
||||
workspaceId: z.string().nullish(),
|
||||
version: z.string(),
|
||||
})
|
||||
.strict();
|
||||
export const baseEventSchema = z.strictObject({
|
||||
timestamp: z.string(),
|
||||
userId: z.string().nullish(),
|
||||
workspaceId: z.string().nullish(),
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
|
||||
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
|
||||
export const objectRecordCreatedSchema = z.object({
|
||||
event: z.literal(OBJECT_RECORD_CREATED_EVENT),
|
||||
properties: z.object({}).passthrough(),
|
||||
properties: z.looseObject({}),
|
||||
});
|
||||
|
||||
export type ObjectRecordCreatedTrackEvent = z.infer<
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
|
||||
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
|
||||
export const objectRecordDeletedSchema = z.object({
|
||||
event: z.literal(OBJECT_RECORD_DELETED_EVENT),
|
||||
properties: z.object({}).passthrough(),
|
||||
properties: z.looseObject({}),
|
||||
});
|
||||
|
||||
export type ObjectRecordDeletedTrackEvent = z.infer<
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
|
||||
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
|
||||
export const objectRecordUpdatedSchema = z.object({
|
||||
event: z.literal(OBJECT_RECORD_UPDATED_EVENT),
|
||||
properties: z.object({}).passthrough(),
|
||||
properties: z.looseObject({}),
|
||||
});
|
||||
|
||||
export type ObjectRecordUpdatedTrackEvent = z.infer<
|
||||
|
||||
+4
-6
@@ -3,12 +3,10 @@ import { z } from 'zod';
|
||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
|
||||
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
|
||||
export const customDomainActivatedSchema = z
|
||||
.object({
|
||||
event: z.literal(CUSTOM_DOMAIN_ACTIVATED_EVENT),
|
||||
properties: z.object({}).strict(),
|
||||
})
|
||||
.strict();
|
||||
export const customDomainActivatedSchema = z.strictObject({
|
||||
event: z.literal(CUSTOM_DOMAIN_ACTIVATED_EVENT),
|
||||
properties: z.strictObject({}),
|
||||
});
|
||||
|
||||
export type CustomDomainActivatedTrackEvent = z.infer<
|
||||
typeof customDomainActivatedSchema
|
||||
|
||||
+4
-6
@@ -4,12 +4,10 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
|
||||
|
||||
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
|
||||
'Custom Domain Deactivated' as const;
|
||||
export const customDomainDeactivatedSchema = z
|
||||
.object({
|
||||
event: z.literal(CUSTOM_DOMAIN_DEACTIVATED_EVENT),
|
||||
properties: z.object({}).strict(),
|
||||
})
|
||||
.strict();
|
||||
export const customDomainDeactivatedSchema = z.strictObject({
|
||||
event: z.literal(CUSTOM_DOMAIN_DEACTIVATED_EVENT),
|
||||
properties: z.strictObject({}),
|
||||
});
|
||||
|
||||
export type CustomDomainDeactivatedTrackEvent = z.infer<
|
||||
typeof customDomainDeactivatedSchema
|
||||
|
||||
+9
-13
@@ -3,19 +3,15 @@ import { z } from 'zod';
|
||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
|
||||
export const MONITORING_EVENT = 'Monitoring' as const;
|
||||
export const monitoringSchema = z
|
||||
.object({
|
||||
event: z.literal(MONITORING_EVENT),
|
||||
properties: z
|
||||
.object({
|
||||
eventName: z.string(),
|
||||
connectedAccountId: z.string().optional(),
|
||||
messageChannelId: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.strict(),
|
||||
})
|
||||
.strict();
|
||||
export const monitoringSchema = z.strictObject({
|
||||
event: z.literal(MONITORING_EVENT),
|
||||
properties: z.strictObject({
|
||||
eventName: z.string(),
|
||||
connectedAccountId: z.string().optional(),
|
||||
messageChannelId: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type MonitoringTrackEvent = z.infer<typeof monitoringSchema>;
|
||||
|
||||
|
||||
+10
-14
@@ -4,20 +4,16 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
|
||||
|
||||
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
|
||||
'Serverless Function Executed' as const;
|
||||
export const serverlessFunctionExecutedSchema = z
|
||||
.object({
|
||||
event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT),
|
||||
properties: z
|
||||
.object({
|
||||
duration: z.number(),
|
||||
status: z.enum(['IDLE', 'SUCCESS', 'ERROR']),
|
||||
errorType: z.string().optional(),
|
||||
functionId: z.string(),
|
||||
functionName: z.string(),
|
||||
})
|
||||
.strict(),
|
||||
})
|
||||
.strict();
|
||||
export const serverlessFunctionExecutedSchema = z.strictObject({
|
||||
event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT),
|
||||
properties: z.strictObject({
|
||||
duration: z.number(),
|
||||
status: z.enum(['IDLE', 'SUCCESS', 'ERROR']),
|
||||
errorType: z.string().optional(),
|
||||
functionId: z.string(),
|
||||
functionName: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type ServerlessFunctionExecutedTrackEvent = z.infer<
|
||||
typeof serverlessFunctionExecutedSchema
|
||||
|
||||
+4
-6
@@ -3,12 +3,10 @@ import { z } from 'zod';
|
||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
|
||||
export const USER_SIGNUP_EVENT = 'User Signup' as const;
|
||||
export const userSignupSchema = z
|
||||
.object({
|
||||
event: z.literal(USER_SIGNUP_EVENT),
|
||||
properties: z.object({}).strict(),
|
||||
})
|
||||
.strict();
|
||||
export const userSignupSchema = z.strictObject({
|
||||
event: z.literal(USER_SIGNUP_EVENT),
|
||||
properties: z.strictObject({}),
|
||||
});
|
||||
|
||||
export type UserSignupTrackEvent = z.infer<typeof userSignupSchema>;
|
||||
|
||||
|
||||
+10
-14
@@ -3,20 +3,16 @@ import { z } from 'zod';
|
||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
|
||||
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
|
||||
export const webhookResponseSchema = z
|
||||
.object({
|
||||
event: z.literal(WEBHOOK_RESPONSE_EVENT),
|
||||
properties: z
|
||||
.object({
|
||||
status: z.number().optional(),
|
||||
success: z.boolean(),
|
||||
url: z.string(),
|
||||
webhookId: z.string(),
|
||||
eventName: z.string(),
|
||||
})
|
||||
.strict(),
|
||||
})
|
||||
.strict();
|
||||
export const webhookResponseSchema = z.strictObject({
|
||||
event: z.literal(WEBHOOK_RESPONSE_EVENT),
|
||||
properties: z.strictObject({
|
||||
status: z.number().optional(),
|
||||
success: z.boolean(),
|
||||
url: z.string(),
|
||||
webhookId: z.string(),
|
||||
eventName: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type WebhookResponseTrackEvent = z.infer<typeof webhookResponseSchema>;
|
||||
|
||||
|
||||
+6
-8
@@ -4,14 +4,12 @@ import { registerEvent } from 'src/engine/core-modules/audit/utils/events/worksp
|
||||
|
||||
export const WORKSPACE_ENTITY_CREATED_EVENT =
|
||||
'Workspace Entity Created' as const;
|
||||
export const workspaceEntityCreatedSchema = z
|
||||
.object({
|
||||
event: z.literal(WORKSPACE_ENTITY_CREATED_EVENT),
|
||||
properties: z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
export const workspaceEntityCreatedSchema = z.strictObject({
|
||||
event: z.literal(WORKSPACE_ENTITY_CREATED_EVENT),
|
||||
properties: z.strictObject({
|
||||
name: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type WorkspaceEntityCreatedTrackEvent = z.infer<
|
||||
typeof workspaceEntityCreatedSchema
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import { type ConnectionParameters } from 'src/engine/core-modules/imap-smtp-cal
|
||||
export class ImapSmtpCaldavValidatorService {
|
||||
private readonly protocolConnectionSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.number().int().positive('Port must be a positive number'),
|
||||
port: z.int().positive('Port must be a positive number'),
|
||||
username: z.string().optional(),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
secure: z.boolean().optional(),
|
||||
@@ -29,7 +29,7 @@ export class ImapSmtpCaldavValidatorService {
|
||||
return this.protocolConnectionSchema.parse(params);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errorMessages = error.errors
|
||||
const errorMessages = error.issues
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export class ToolRegistryService {
|
||||
ToolType.SEND_EMAIL,
|
||||
() => ({
|
||||
description: this.sendEmailTool.description,
|
||||
parameters: this.sendEmailTool.parameters,
|
||||
inputSchema: this.sendEmailTool.inputSchema,
|
||||
execute: (params) =>
|
||||
this.sendEmailTool.execute(params as SendEmailInput),
|
||||
flag: PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export const HttpRequestInputZodSchema = z.object({
|
||||
.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
.describe('The HTTP method to use'),
|
||||
headers: z
|
||||
.record(z.string())
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe('HTTP headers to include in the request'),
|
||||
body: z
|
||||
|
||||
@@ -13,7 +13,7 @@ import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
export class HttpTool implements Tool {
|
||||
description =
|
||||
'Make an HTTP request to any URL with configurable method, headers, and body.';
|
||||
parameters = HttpToolParametersZodSchema;
|
||||
inputSchema = HttpToolParametersZodSchema;
|
||||
|
||||
async execute(parameters: ToolInput): Promise<ToolOutput> {
|
||||
const { url, method, headers, body } = parameters as HttpRequestInput;
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SendEmailInputZodSchema = z.object({
|
||||
email: z.string().email().describe('The recipient email address'),
|
||||
email: z.email().describe('The recipient email address'),
|
||||
subject: z.string().describe('The email subject line'),
|
||||
body: z.string().describe('The email body content (HTML or plain text)'),
|
||||
connectedAccountId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The UUID of the connected account to send the email from. Provide this only if you have it; otherwise, leave blank.',
|
||||
|
||||
+5
-2
@@ -26,7 +26,7 @@ export class SendEmailTool implements Tool {
|
||||
|
||||
description =
|
||||
'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.';
|
||||
parameters = SendEmailToolParametersZodSchema;
|
||||
inputSchema = SendEmailToolParametersZodSchema;
|
||||
|
||||
constructor(
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
@@ -91,7 +91,10 @@ export class SendEmailTool implements Tool {
|
||||
let { connectedAccountId } = parameters;
|
||||
|
||||
try {
|
||||
const emailSchema = z.string().trim().email('Invalid email');
|
||||
const emailSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.pipe(z.email({ error: 'Invalid email' }));
|
||||
const emailValidation = emailSchema.safeParse(email);
|
||||
|
||||
if (!emailValidation.success) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type JSONSchema7 } from 'json-schema';
|
||||
import { type ZodType } from 'zod';
|
||||
import { type FlexibleSchema } from '@ai-sdk/provider-utils';
|
||||
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
@@ -7,7 +6,7 @@ import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions
|
||||
|
||||
export type Tool = {
|
||||
description: string;
|
||||
parameters: JSONSchema7 | ZodType;
|
||||
inputSchema: FlexibleSchema<unknown>;
|
||||
execute(input: ToolInput): Promise<ToolOutput>;
|
||||
flag?: PermissionFlagType;
|
||||
};
|
||||
|
||||
+16
-21
@@ -33,33 +33,28 @@ export type TOTPStrategyConfig = z.infer<typeof TOTP_STRATEGY_CONFIG_SCHEMA>;
|
||||
|
||||
export const TOTP_STRATEGY_CONFIG_SCHEMA = z.object({
|
||||
algorithm: z
|
||||
.nativeEnum(TOTPHashAlgorithms, {
|
||||
errorMap: () => ({
|
||||
message:
|
||||
'Invalid algorithm specified. Must be SHA1, SHA256, or SHA512.',
|
||||
}),
|
||||
.enum(TOTPHashAlgorithms, {
|
||||
error: () =>
|
||||
'Invalid algorithm specified. Must be SHA1, SHA256, or SHA512.',
|
||||
})
|
||||
.optional(),
|
||||
digits: z
|
||||
.number({
|
||||
invalid_type_error: 'Digits must be a number.',
|
||||
.int({
|
||||
error: 'Digits must be a whole number.',
|
||||
})
|
||||
.min(6, {
|
||||
error: 'Digits must be at least 6.',
|
||||
})
|
||||
.max(8, {
|
||||
error: 'Digits cannot be more than 8.',
|
||||
})
|
||||
.int({ message: 'Digits must be a whole number.' })
|
||||
.min(6, { message: 'Digits must be at least 6.' })
|
||||
.max(8, { message: 'Digits cannot be more than 8.' })
|
||||
.optional(),
|
||||
encodings: z
|
||||
.nativeEnum(TOTPKeyEncodings, {
|
||||
errorMap: () => ({ message: 'Invalid encoding specified.' }),
|
||||
.enum(TOTPKeyEncodings, {
|
||||
error: () => 'Invalid encoding specified.',
|
||||
})
|
||||
.optional(),
|
||||
window: z.number().int().min(0).optional(),
|
||||
step: z
|
||||
.number({
|
||||
invalid_type_error: 'Step must be a number.',
|
||||
})
|
||||
.int()
|
||||
.min(1)
|
||||
.optional(),
|
||||
epoch: z.number().int().min(0).optional(),
|
||||
window: z.int().min(0).optional(),
|
||||
step: z.int().min(1).optional(),
|
||||
epoch: z.int().min(0).optional(),
|
||||
});
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { authenticator } from 'otplib';
|
||||
import { TwoFactorAuthenticationStrategy } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type SafeParseReturnType } from 'zod';
|
||||
import { type ZodSafeParseResult } from 'zod';
|
||||
|
||||
import { type OTPAuthenticationStrategyInterface } from 'src/engine/core-modules/two-factor-authentication/strategies/otp/interfaces/otp.strategy.interface';
|
||||
|
||||
@@ -24,7 +24,7 @@ export class TotpStrategy implements OTPAuthenticationStrategyInterface {
|
||||
public readonly name = TwoFactorAuthenticationStrategy.TOTP;
|
||||
|
||||
constructor(options?: TOTPStrategyConfig) {
|
||||
let result: SafeParseReturnType<unknown, TOTPStrategyConfig> | undefined;
|
||||
let result: ZodSafeParseResult<TOTPStrategyConfig> | undefined;
|
||||
|
||||
if (isDefined(options)) {
|
||||
result = TOTP_STRATEGY_CONFIG_SCHEMA.safeParse(options);
|
||||
|
||||
+13
-18
@@ -2,13 +2,14 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
type CoreMessage,
|
||||
type CoreUserMessage,
|
||||
type FilePart,
|
||||
type ImagePart,
|
||||
LanguageModelUsage,
|
||||
type ModelMessage,
|
||||
streamText,
|
||||
ToolSet,
|
||||
type UserContent,
|
||||
UserModelMessage,
|
||||
} from 'ai';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
@@ -40,11 +41,7 @@ import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
|
||||
export interface AgentExecutionResult {
|
||||
result: object;
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
usage: LanguageModelUsage;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -68,14 +65,12 @@ export class AgentExecutionService {
|
||||
|
||||
async prepareAIRequestConfig({
|
||||
messages,
|
||||
prompt,
|
||||
system,
|
||||
agent,
|
||||
}: {
|
||||
system: string;
|
||||
agent: AgentEntity | null;
|
||||
prompt?: string;
|
||||
messages?: CoreMessage[];
|
||||
messages: ModelMessage[];
|
||||
}) {
|
||||
try {
|
||||
if (agent) {
|
||||
@@ -111,8 +106,7 @@ export class AgentExecutionService {
|
||||
system,
|
||||
tools,
|
||||
model: registeredModel.model,
|
||||
...(messages && { messages }),
|
||||
...(prompt && { prompt }),
|
||||
messages,
|
||||
maxSteps: AGENT_CONFIG.MAX_STEPS,
|
||||
...(registeredModel.doesSupportThinking && {
|
||||
providerOptions: {
|
||||
@@ -149,7 +143,7 @@ export class AgentExecutionService {
|
||||
private async buildUserMessage(
|
||||
userMessage: string,
|
||||
fileIds: string[],
|
||||
): Promise<CoreUserMessage> {
|
||||
): Promise<UserModelMessage> {
|
||||
const content: Exclude<UserContent, string> = [
|
||||
{
|
||||
type: 'text',
|
||||
@@ -248,22 +242,22 @@ export class AgentExecutionService {
|
||||
return {
|
||||
type: 'image',
|
||||
image: fileBuffer,
|
||||
mimeType: file.type,
|
||||
mediaType: file.type,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'file',
|
||||
data: fileBuffer,
|
||||
mimeType: file.type,
|
||||
mediaType: file.type,
|
||||
};
|
||||
}
|
||||
|
||||
private mapMessagesToCoreMessages(
|
||||
messages: AgentChatMessageEntity[],
|
||||
): CoreMessage[] {
|
||||
): ModelMessage[] {
|
||||
return messages
|
||||
.map(({ role, rawContent }): CoreMessage => {
|
||||
.map(({ role, rawContent }): ModelMessage => {
|
||||
if (role === AgentChatMessageRole.USER) {
|
||||
return {
|
||||
role: 'user',
|
||||
@@ -300,7 +294,8 @@ export class AgentExecutionService {
|
||||
where: { id: agentId },
|
||||
});
|
||||
|
||||
const llmMessages: CoreMessage[] = this.mapMessagesToCoreMessages(messages);
|
||||
const llmMessages: ModelMessage[] =
|
||||
this.mapMessagesToCoreMessages(messages);
|
||||
|
||||
let contextString = '';
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { CoreMessage, generateText } from 'ai';
|
||||
import { ModelMessage, generateText } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
@@ -15,7 +15,7 @@ export type HandoffRequest = {
|
||||
fromAgentId: string;
|
||||
toAgentId: string;
|
||||
workspaceId: string;
|
||||
messages?: CoreMessage[];
|
||||
messages: ModelMessage[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export class AgentHandoffToolService {
|
||||
'{agentName}',
|
||||
handoff.toAgent.name,
|
||||
),
|
||||
parameters: AGENT_HANDOFF_SCHEMA,
|
||||
inputSchema: AGENT_HANDOFF_SCHEMA,
|
||||
execute: async ({ input }) => {
|
||||
const result = await this.agentHandoffExecutorService.executeHandoff({
|
||||
fromAgentId: agentId,
|
||||
|
||||
+11
-8
@@ -25,6 +25,16 @@ export type StreamAgentChatOptions = {
|
||||
res: Response;
|
||||
};
|
||||
|
||||
const CLIENT_FORWARDED_EVENT_TYPES = [
|
||||
'text-delta',
|
||||
'reasoning',
|
||||
'reasoning-delta',
|
||||
'tool-call',
|
||||
'tool-input-delta',
|
||||
'tool-result',
|
||||
'error',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class AgentStreamingService {
|
||||
private readonly logger = new Logger(AgentStreamingService.name);
|
||||
@@ -81,14 +91,7 @@ export class AgentStreamingService {
|
||||
|
||||
this.sendStreamEvent(
|
||||
res,
|
||||
[
|
||||
'text-delta',
|
||||
'reasoning',
|
||||
'reasoning-signature',
|
||||
'tool-call',
|
||||
'tool-result',
|
||||
'error',
|
||||
].includes(chunk.type)
|
||||
CLIENT_FORWARDED_EVENT_TYPES.includes(chunk.type)
|
||||
? chunk
|
||||
: { type: chunk.type },
|
||||
);
|
||||
|
||||
+10
-22
@@ -26,24 +26,16 @@ export const AGENT_HANDOFF_SCHEMA = z.object({
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('image'),
|
||||
image: z.union([
|
||||
z.string(),
|
||||
z.instanceof(Uint8Array),
|
||||
z.instanceof(Buffer),
|
||||
z.instanceof(ArrayBuffer),
|
||||
z.string().url(),
|
||||
]),
|
||||
image: z
|
||||
.string()
|
||||
.describe('Base64 encoded image data or 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(),
|
||||
]),
|
||||
data: z
|
||||
.string()
|
||||
.describe('Base64 encoded file data or URL'),
|
||||
mediaType: z.string(),
|
||||
}),
|
||||
]),
|
||||
@@ -62,13 +54,9 @@ export const AGENT_HANDOFF_SCHEMA = z.object({
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('file'),
|
||||
data: z.union([
|
||||
z.string(),
|
||||
z.instanceof(Uint8Array),
|
||||
z.instanceof(Buffer),
|
||||
z.instanceof(ArrayBuffer),
|
||||
z.string().url(),
|
||||
]),
|
||||
data: z
|
||||
.string()
|
||||
.describe('Base64 encoded file data or URL'),
|
||||
mediaType: z.string(),
|
||||
filename: z.string().optional(),
|
||||
}),
|
||||
@@ -80,7 +68,7 @@ export const AGENT_HANDOFF_SCHEMA = z.object({
|
||||
type: z.literal('tool-call'),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
input: z.record(z.any()),
|
||||
input: z.record(z.string(), z.any()),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
|
||||
+66
-30
@@ -1,60 +1,96 @@
|
||||
import { type ReasoningPart } from '@ai-sdk/provider-utils';
|
||||
import { type TextPart } from 'ai';
|
||||
|
||||
type ReasoningPart = {
|
||||
type: 'reasoning';
|
||||
text: string;
|
||||
signature: string;
|
||||
};
|
||||
import {
|
||||
parseStreamLine,
|
||||
splitStreamIntoLines,
|
||||
type TextBlock,
|
||||
} from 'twenty-shared/ai';
|
||||
|
||||
export const constructAssistantMessageContentFromStream = (
|
||||
rawContent: string,
|
||||
) => {
|
||||
const lines = rawContent.trim().split('\n');
|
||||
const lines = splitStreamIntoLines(rawContent);
|
||||
|
||||
const output: Array<TextPart | ReasoningPart> = [];
|
||||
let reasoningText = '';
|
||||
let textContent = '';
|
||||
let currentTextBlock: TextBlock = null;
|
||||
|
||||
const flushTextBlock = () => {
|
||||
if (currentTextBlock) {
|
||||
if (currentTextBlock.type === 'reasoning') {
|
||||
output.push({
|
||||
type: 'reasoning',
|
||||
text: currentTextBlock.content,
|
||||
});
|
||||
} else {
|
||||
output.push({
|
||||
type: 'text',
|
||||
text: currentTextBlock.content,
|
||||
});
|
||||
}
|
||||
currentTextBlock = null;
|
||||
}
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
let event;
|
||||
const event = parseStreamLine(line);
|
||||
|
||||
try {
|
||||
event = JSON.parse(line);
|
||||
} catch {
|
||||
if (!event) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'reasoning':
|
||||
reasoningText += event.textDelta || '';
|
||||
case 'reasoning-start':
|
||||
flushTextBlock();
|
||||
currentTextBlock = {
|
||||
type: 'reasoning',
|
||||
content: '',
|
||||
isThinking: true,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'reasoning-signature':
|
||||
if (reasoningText) {
|
||||
output.push({
|
||||
case 'reasoning-delta':
|
||||
if (!currentTextBlock || currentTextBlock.type !== 'reasoning') {
|
||||
flushTextBlock();
|
||||
currentTextBlock = {
|
||||
type: 'reasoning',
|
||||
text: reasoningText,
|
||||
signature: event.signature,
|
||||
});
|
||||
reasoningText = '';
|
||||
content: '',
|
||||
isThinking: true,
|
||||
};
|
||||
}
|
||||
currentTextBlock.content += event.text || '';
|
||||
break;
|
||||
|
||||
case 'reasoning-end':
|
||||
if (currentTextBlock?.type === 'reasoning') {
|
||||
currentTextBlock.isThinking = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text-delta':
|
||||
textContent += event.textDelta || '';
|
||||
if (!currentTextBlock || currentTextBlock.type !== 'text') {
|
||||
flushTextBlock();
|
||||
currentTextBlock = { type: 'text', content: '' };
|
||||
}
|
||||
currentTextBlock.content += event.text || '';
|
||||
break;
|
||||
|
||||
case 'step-finish':
|
||||
if (currentTextBlock?.type === 'reasoning') {
|
||||
currentTextBlock.isThinking = false;
|
||||
}
|
||||
flushTextBlock();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
flushTextBlock();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (textContent) {
|
||||
output.push({
|
||||
type: 'text',
|
||||
text: textContent,
|
||||
});
|
||||
textContent = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flushTextBlock();
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ export class BlocklistValidationService {
|
||||
const emailOrDomainSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.email('Invalid email or domain')
|
||||
.pipe(z.email({ error: 'Invalid email or domain' }))
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
@@ -73,7 +73,7 @@ export class BlocklistValidationService {
|
||||
const result = emailOrDomainSchema.safeParse(handle);
|
||||
|
||||
if (!result.success) {
|
||||
throw new BadRequestException(result.error.errors[0].message);
|
||||
throw new BadRequestException(result.error.issues[0].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { generateObject, generateText, ToolSet } from 'ai';
|
||||
import { generateObject, generateText, stepCountIs, ToolSet } from 'ai';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
@@ -98,7 +98,7 @@ export class AiAgentExecutorService {
|
||||
tools,
|
||||
model: registeredModel.model,
|
||||
prompt: userPrompt,
|
||||
maxSteps: AGENT_CONFIG.MAX_STEPS,
|
||||
stopWhen: stepCountIs(AGENT_CONFIG.MAX_STEPS),
|
||||
});
|
||||
|
||||
if (Object.keys(schema).length === 0) {
|
||||
@@ -121,12 +121,12 @@ export class AiAgentExecutorService {
|
||||
return {
|
||||
result: output.object,
|
||||
usage: {
|
||||
promptTokens:
|
||||
(textResponse.usage?.promptTokens ?? 0) +
|
||||
(output.usage?.promptTokens ?? 0),
|
||||
completionTokens:
|
||||
(textResponse.usage?.completionTokens ?? 0) +
|
||||
(output.usage?.completionTokens ?? 0),
|
||||
inputTokens:
|
||||
(textResponse.usage?.inputTokens ?? 0) +
|
||||
(output.usage?.inputTokens ?? 0),
|
||||
outputTokens:
|
||||
(textResponse.usage?.outputTokens ?? 0) +
|
||||
(output.usage?.outputTokens ?? 0),
|
||||
totalTokens:
|
||||
(textResponse.usage?.totalTokens ?? 0) +
|
||||
(output.usage?.totalTokens ?? 0),
|
||||
|
||||
+2
-4
@@ -1,11 +1,9 @@
|
||||
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';
|
||||
|
||||
import { workflowActionSchema, workflowTriggerSchema } from './workflow.schema';
|
||||
|
||||
export const createWorkflowVersionStepSchema = z.object({
|
||||
workflowVersionId: z
|
||||
.string()
|
||||
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
StepLogicalOperator,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const objectRecordSchema = z
|
||||
.record(z.string(), 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
|
||||
.looseObject({})
|
||||
.describe(
|
||||
'Input data for the workflow action. Structure depends on the action type.',
|
||||
),
|
||||
outputSchema: z
|
||||
.looseObject({})
|
||||
.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().describe('Whether to retry the action if it fails.'),
|
||||
}),
|
||||
continueOnFailure: z.object({
|
||||
value: z
|
||||
.boolean()
|
||||
.describe('Whether to continue to the next step if this action fails.'),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const baseWorkflowActionSchema = z.object({
|
||||
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()
|
||||
.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.',
|
||||
),
|
||||
});
|
||||
|
||||
export const workflowCodeActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
serverlessFunctionId: z.string(),
|
||||
serverlessFunctionVersion: z.string(),
|
||||
serverlessFunctionInput: z.record(z.string(), z.any()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowSendEmailActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
connectedAccountId: z.string(),
|
||||
email: z.string(),
|
||||
subject: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowCreateRecordActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
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.'),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowUpdateRecordActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
objectRecord: objectRecordSchema,
|
||||
objectRecordId: z.string(),
|
||||
fieldsToUpdate: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowDeleteRecordActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
objectRecordId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowFindRecordsActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
limit: z.number().optional(),
|
||||
filter: z
|
||||
.object({
|
||||
recordFilterGroups: z.array(z.object({})).optional(),
|
||||
recordFilters: z.array(z.object({})).optional(),
|
||||
gqlOperationFilter: z.object({}).optional().nullable(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowFormActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
label: z.string(),
|
||||
type: z.union([
|
||||
z.literal(FieldMetadataType.TEXT),
|
||||
z.literal(FieldMetadataType.NUMBER),
|
||||
z.literal(FieldMetadataType.DATE),
|
||||
z.literal(FieldMetadataType.SELECT),
|
||||
z.literal('RECORD'),
|
||||
]),
|
||||
placeholder: z.string().optional(),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
value: z.any().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const workflowHttpRequestActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
url: z.string(),
|
||||
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
body: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.null(),
|
||||
z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
|
||||
]),
|
||||
)
|
||||
.or(z.string())
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowAiAgentActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
agentId: z.string().optional(),
|
||||
prompt: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowFilterActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
stepFilterGroups: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
logicalOperator: z.enum(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.enum(ViewFilterOperand),
|
||||
value: z.string(),
|
||||
stepFilterGroupId: z.string(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
fieldMetadataId: z.string().optional(),
|
||||
compositeFieldSubFieldName: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowIteratorActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
items: z
|
||||
.union([
|
||||
z.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.null(),
|
||||
z.record(z.string(), z.any()),
|
||||
z.any(),
|
||||
]),
|
||||
),
|
||||
z.string(),
|
||||
])
|
||||
.optional(),
|
||||
initialLoopStepIds: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowEmptyActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({}),
|
||||
});
|
||||
|
||||
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('CODE'),
|
||||
settings: workflowCodeActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('SEND_EMAIL'),
|
||||
settings: workflowSendEmailActionSettingsSchema,
|
||||
});
|
||||
|
||||
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 workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||
{
|
||||
type: z.literal('DELETE_RECORD'),
|
||||
settings: workflowDeleteRecordActionSettingsSchema,
|
||||
},
|
||||
);
|
||||
|
||||
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('FIND_RECORDS'),
|
||||
settings: workflowFindRecordsActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowFormActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('FORM'),
|
||||
settings: workflowFormActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('HTTP_REQUEST'),
|
||||
settings: workflowHttpRequestActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('AI_AGENT'),
|
||||
settings: workflowAiAgentActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('FILTER'),
|
||||
settings: workflowFilterActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('ITERATOR'),
|
||||
settings: workflowIteratorActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('EMPTY'),
|
||||
settings: workflowEmptyActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowCodeActionSchema,
|
||||
workflowSendEmailActionSchema,
|
||||
workflowCreateRecordActionSchema,
|
||||
workflowUpdateRecordActionSchema,
|
||||
workflowDeleteRecordActionSchema,
|
||||
workflowFindRecordsActionSchema,
|
||||
workflowFormActionSchema,
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowIteratorActionSchema,
|
||||
workflowEmptyActionSchema,
|
||||
]);
|
||||
|
||||
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema
|
||||
.extend({
|
||||
type: z.literal('DATABASE_EVENT'),
|
||||
settings: z.object({
|
||||
eventName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-z][a-zA-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.looseObject({}).optional(),
|
||||
outputSchema: z
|
||||
.looseObject({})
|
||||
.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
|
||||
.looseObject({})
|
||||
.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'),
|
||||
settings: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('DAYS'),
|
||||
schedule: z.object({
|
||||
day: z.number().min(1),
|
||||
hour: z.number().min(0).max(23),
|
||||
minute: z.number().min(0).max(59),
|
||||
}),
|
||||
outputSchema: z.looseObject({}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('HOURS'),
|
||||
schedule: z.object({
|
||||
hour: z.number().min(1),
|
||||
minute: z.number().min(0).max(59),
|
||||
}),
|
||||
outputSchema: z.looseObject({}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('MINUTES'),
|
||||
schedule: z.object({ minute: z.number().min(1) }),
|
||||
outputSchema: z.looseObject({}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('CUSTOM'),
|
||||
pattern: z.string(),
|
||||
outputSchema: z.looseObject({}),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({
|
||||
type: z.literal('WEBHOOK'),
|
||||
settings: z.discriminatedUnion('httpMethod', [
|
||||
z.object({
|
||||
outputSchema: z.looseObject({}),
|
||||
httpMethod: z.literal('GET'),
|
||||
authentication: z.literal('API_KEY').nullable(),
|
||||
}),
|
||||
z.object({
|
||||
outputSchema: z.looseObject({}),
|
||||
httpMethod: z.literal('POST'),
|
||||
expectedBody: z.looseObject({}),
|
||||
authentication: z.literal('API_KEY').nullable(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
export const workflowTriggerSchema = z.discriminatedUnion('type', [
|
||||
workflowDatabaseEventTriggerSchema,
|
||||
workflowManualTriggerSchema,
|
||||
workflowCronTriggerSchema,
|
||||
workflowWebhookTriggerSchema,
|
||||
]);
|
||||
|
||||
export const workflowRunStepStatusSchema = z.enum(StepStatus);
|
||||
|
||||
export const workflowRunStateStepInfoSchema = z.object({
|
||||
result: z.any().optional(),
|
||||
error: z.any().optional(),
|
||||
status: workflowRunStepStatusSchema,
|
||||
});
|
||||
|
||||
export const workflowRunStateStepInfosSchema = z.record(
|
||||
z.string(),
|
||||
workflowRunStateStepInfoSchema,
|
||||
);
|
||||
|
||||
export const workflowRunStateSchema = z.object({
|
||||
flow: z.object({
|
||||
trigger: workflowTriggerSchema,
|
||||
steps: z.array(workflowActionSchema),
|
||||
}),
|
||||
stepInfos: workflowRunStateStepInfosSchema,
|
||||
workflowRunError: z.any().optional(),
|
||||
});
|
||||
|
||||
export const workflowRunStatusSchema = z.enum([
|
||||
'NOT_STARTED',
|
||||
'RUNNING',
|
||||
'COMPLETED',
|
||||
'FAILED',
|
||||
'ENQUEUED',
|
||||
]);
|
||||
|
||||
export const workflowRunSchema = z.looseObject({
|
||||
__typename: z.literal('WorkflowRun'),
|
||||
id: z.string(),
|
||||
workflowVersionId: z.string(),
|
||||
workflowId: z.string(),
|
||||
state: workflowRunStateSchema.nullable(),
|
||||
status: workflowRunStatusSchema,
|
||||
createdAt: z.string(),
|
||||
deletedAt: z.string().nullable(),
|
||||
endedAt: z.string().nullable(),
|
||||
name: z.string(),
|
||||
});
|
||||
+11
-11
@@ -71,7 +71,7 @@ IMPORTANT: The tool schema provides comprehensive field descriptions, examples,
|
||||
- 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,
|
||||
inputSchema: createCompleteWorkflowSchema,
|
||||
execute: async (parameters: {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -160,7 +160,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
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,
|
||||
inputSchema: createWorkflowVersionStepSchema,
|
||||
execute: async (parameters: CreateWorkflowVersionStepInput) => {
|
||||
try {
|
||||
return await this.workflowVersionStepService.createWorkflowVersionStep(
|
||||
@@ -182,7 +182,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
tools.update_workflow_version_step = {
|
||||
description:
|
||||
'Update an existing step in a workflow version. This modifies the step configuration.',
|
||||
parameters: updateWorkflowVersionStepSchema,
|
||||
inputSchema: updateWorkflowVersionStepSchema,
|
||||
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
|
||||
try {
|
||||
return await this.workflowVersionStepService.updateWorkflowVersionStep(
|
||||
@@ -205,7 +205,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
tools.delete_workflow_version_step = {
|
||||
description:
|
||||
'Delete a step from a workflow version. This removes the step and updates the workflow structure.',
|
||||
parameters: deleteWorkflowVersionStepSchema,
|
||||
inputSchema: deleteWorkflowVersionStepSchema,
|
||||
execute: async (parameters: {
|
||||
workflowVersionId: string;
|
||||
stepId: string;
|
||||
@@ -231,7 +231,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
tools.create_workflow_version_edge = {
|
||||
description:
|
||||
'Create a connection (edge) between two workflow steps. This defines the flow between steps.',
|
||||
parameters: createWorkflowVersionEdgeSchema,
|
||||
inputSchema: createWorkflowVersionEdgeSchema,
|
||||
execute: async (parameters: {
|
||||
workflowVersionId: string;
|
||||
source: string;
|
||||
@@ -258,7 +258,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
|
||||
tools.delete_workflow_version_edge = {
|
||||
description: 'Delete a connection (edge) between workflow steps.',
|
||||
parameters: deleteWorkflowVersionEdgeSchema,
|
||||
inputSchema: deleteWorkflowVersionEdgeSchema,
|
||||
execute: async (parameters: {
|
||||
workflowVersionId: string;
|
||||
source: string;
|
||||
@@ -286,7 +286,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
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,
|
||||
inputSchema: createDraftFromWorkflowVersionSchema,
|
||||
execute: async (parameters: {
|
||||
workflowId: string;
|
||||
workflowVersionIdToCopy: string;
|
||||
@@ -312,7 +312,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
tools.update_workflow_version_positions = {
|
||||
description:
|
||||
'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.',
|
||||
parameters: updateWorkflowVersionPositionsSchema,
|
||||
inputSchema: updateWorkflowVersionPositionsSchema,
|
||||
execute: async (parameters: UpdateWorkflowVersionPositionsInput) => {
|
||||
try {
|
||||
return await this.workflowVersionService.updateWorkflowVersionPositions(
|
||||
@@ -335,7 +335,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
tools.activate_workflow_version = {
|
||||
description:
|
||||
'Activate a workflow version. This makes the workflow version active and available for execution.',
|
||||
parameters: activateWorkflowVersionSchema,
|
||||
inputSchema: activateWorkflowVersionSchema,
|
||||
execute: async (parameters: { workflowVersionId: string }) => {
|
||||
try {
|
||||
return await this.workflowTriggerService.activateWorkflowVersion(
|
||||
@@ -354,7 +354,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
tools.deactivate_workflow_version = {
|
||||
description:
|
||||
'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.',
|
||||
parameters: deactivateWorkflowVersionSchema,
|
||||
inputSchema: deactivateWorkflowVersionSchema,
|
||||
execute: async (parameters: { workflowVersionId: string }) => {
|
||||
try {
|
||||
return await this.workflowTriggerService.deactivateWorkflowVersion(
|
||||
@@ -373,7 +373,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
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,
|
||||
inputSchema: computeStepOutputSchemaSchema,
|
||||
execute: async (parameters: {
|
||||
step: WorkflowTrigger | WorkflowAction;
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user