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:
+14
-1
@@ -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;
|
||||
|
||||
|
||||
+8
@@ -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 () => {
|
||||
|
||||
+36
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user