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

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

---------

Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Co-authored-by: Joseph Chiang <josephj6802@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Ajay A Adsule <103304466+AjayAdsule@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Marty <91310557+real-marty@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-06-23 01:12:04 +05:30
committed by GitHub
parent 22e126869c
commit 65df511179
75 changed files with 2268 additions and 30 deletions
@@ -2,6 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import {
ModelId,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
import { ClientConfigController } from './client-config.controller';
@@ -44,6 +48,15 @@ describe('ClientConfigController', () => {
},
],
},
aiModels: [
{
modelId: 'gpt-4o' as ModelId,
label: 'GPT-4o',
provider: ModelProvider.OPENAI,
inputCostPer1kTokensInCredits: 2.5,
outputCostPer1kTokensInCredits: 10.0,
},
],
authProviders: {
google: true,
magicLink: false,
@@ -92,8 +105,8 @@ describe('ClientConfigController', () => {
const result = await controller.getClientConfig();
expect(clientConfigService.getClientConfig).toHaveBeenCalled();
expect(result).toEqual(mockClientConfig);
expect(clientConfigService.getClientConfig).toHaveBeenCalled();
});
});
});
@@ -2,6 +2,10 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import {
ModelId,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@@ -11,6 +15,28 @@ registerEnumType(FeatureFlagKey, {
name: 'FeatureFlagKey',
});
registerEnumType(ModelProvider, {
name: 'ModelProvider',
});
@ObjectType()
export class ClientAIModelConfig {
@Field(() => String)
modelId: ModelId;
@Field(() => String)
label: string;
@Field(() => ModelProvider)
provider: ModelProvider;
@Field(() => Number)
inputCostPer1kTokensInCredits: number;
@Field(() => Number)
outputCostPer1kTokensInCredits: number;
}
@ObjectType()
class Billing {
@Field(() => Boolean)
@@ -88,6 +114,9 @@ export class ClientConfig {
@Field(() => Billing, { nullable: false })
billing: Billing;
@Field(() => [ClientAIModelConfig])
aiModels: ClientAIModelConfig[];
@Field(() => Boolean)
signInPrefilled: boolean;
@@ -9,6 +9,10 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
jest.mock('src/engine/core-modules/ai/constants/ai-models.const', () => ({
AI_MODELS: [],
}));
describe('ClientConfigService', () => {
let service: ClientConfigService;
let twentyConfigService: TwentyConfigService;
@@ -107,6 +111,7 @@ describe('ClientConfigService', () => {
},
],
},
aiModels: [],
authProviders: {
google: true,
magicLink: false,
@@ -164,6 +169,7 @@ describe('ClientConfigService', () => {
expect(result.debugMode).toBe(false);
expect(result.canManageFeatureFlags).toBe(false);
expect(result.aiModels).toEqual([]);
});
it('should handle missing captcha driver', async () => {
@@ -180,6 +186,7 @@ describe('ClientConfigService', () => {
expect(result.captcha.provider).toBeUndefined();
expect(result.captcha.siteKey).toBe('site-key');
expect(result.aiModels).toEqual([]);
});
it('should handle missing support driver', async () => {
@@ -194,6 +201,7 @@ describe('ClientConfigService', () => {
const result = await service.getClientConfig();
expect(result.support.supportDriver).toBe(SupportDriver.NONE);
expect(result.aiModels).toEqual([]);
});
it('should handle billing enabled with feature flags', async () => {
@@ -3,7 +3,15 @@ import { Injectable } from '@nestjs/common';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity';
import {
AI_MODELS,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
import { convertCentsToCredits } from 'src/engine/core-modules/ai/utils/ai-cost.utils';
import {
ClientAIModelConfig,
ClientConfig,
} from 'src/engine/core-modules/client-config/client-config.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -18,6 +26,32 @@ export class ClientConfigService {
async getClientConfig(): Promise<ClientConfig> {
const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER');
const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER');
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
const anthropicApiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
const aiModels = AI_MODELS.reduce<ClientAIModelConfig[]>((acc, model) => {
const isAvailable =
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
if (!isAvailable) {
return acc;
}
acc.push({
modelId: model.modelId,
label: model.label,
provider: model.provider,
inputCostPer1kTokensInCredits: convertCentsToCredits(
model.inputCostPer1kTokensInCents,
),
outputCostPer1kTokensInCredits: convertCentsToCredits(
model.outputCostPer1kTokensInCents,
),
});
return acc;
}, []);
const clientConfig: ClientConfig = {
billing: {
@@ -38,6 +72,7 @@ export class ClientConfigService {
},
],
},
aiModels,
authProviders: {
google: this.twentyConfigService.get('AUTH_GOOGLE_ENABLED'),
magicLink: false,