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:
@ -0,0 +1,131 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { generateObject } from 'ai';
|
||||
|
||||
import {
|
||||
ModelId,
|
||||
ModelProvider,
|
||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||
|
||||
import { AgentEntity } from './agent.entity';
|
||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||
|
||||
import { convertOutputSchemaToZod } from './utils/convert-output-schema-to-zod';
|
||||
|
||||
export interface AgentExecutionResult {
|
||||
object: object;
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AgentExecutionService {
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
private getModel = (modelId: ModelId, provider: ModelProvider) => {
|
||||
switch (provider) {
|
||||
case ModelProvider.OPENAI: {
|
||||
const OpenAIProvider = createOpenAI({
|
||||
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||
});
|
||||
|
||||
return OpenAIProvider(modelId);
|
||||
}
|
||||
case ModelProvider.ANTHROPIC: {
|
||||
const AnthropicProvider = createAnthropic({
|
||||
apiKey: this.twentyConfigService.get('ANTHROPIC_API_KEY'),
|
||||
});
|
||||
|
||||
return AnthropicProvider(modelId);
|
||||
}
|
||||
default:
|
||||
throw new AgentException(
|
||||
`Unsupported provider: ${provider}`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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:
|
||||
throw new AgentException(
|
||||
`Unsupported provider: ${provider}`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new AgentException(
|
||||
`${provider.toUpperCase()} API key not configured`,
|
||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async executeAgent({
|
||||
agent,
|
||||
context,
|
||||
schema,
|
||||
}: {
|
||||
agent: AgentEntity;
|
||||
context: Record<string, unknown>;
|
||||
schema: OutputSchema;
|
||||
}): Promise<AgentExecutionResult> {
|
||||
try {
|
||||
const aiModel = getAIModelById(agent.modelId);
|
||||
|
||||
if (!aiModel) {
|
||||
throw new AgentException(
|
||||
`AI model with id ${agent.modelId} not found`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const provider = aiModel.provider;
|
||||
|
||||
await this.validateApiKey(provider);
|
||||
|
||||
const output = await generateObject({
|
||||
model: this.getModel(agent.modelId, provider),
|
||||
prompt: resolveInput(agent.prompt, context) as string,
|
||||
schema: convertOutputSchemaToZod(schema),
|
||||
});
|
||||
|
||||
return {
|
||||
object: output.object,
|
||||
usage: {
|
||||
promptTokens: output.usage?.promptTokens ?? 0,
|
||||
completionTokens: output.usage?.completionTokens ?? 0,
|
||||
totalTokens: output.usage?.totalTokens,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AgentException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AgentException(
|
||||
error instanceof Error ? error.message : 'Agent execution failed',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user