feat(workflow): pin concrete model on AI agent node creation and exclude interactive tools from workflow runs (#23447)
## Context The AI Agent workflow node's model dropdown could show a model that was not the one used at run time (e.g. the node displayed "Claude Haiku 4.5" while the run log showed `openai/gpt-5.6-sol`). Root cause: workflow agents were created with `modelId: AUTO_SELECT_SMART_MODEL_ID`. The builder's model `Select` cannot represent that value — auto-select ids are filtered out of the options (`useWorkspaceAiModelAvailability`) and the pinned "default" option remaps its value to the resolved concrete model id (`useAiModelOptions`) — so `Select` silently fell back to `options[0]`, the alphabetically first enabled model. Meanwhile the runtime correctly resolved auto-select to the instance's default smart model. ## What this PR does ### 1. New workflow agents store a concrete model id `WorkflowVersionStepOperationsWorkspaceService` now reads the workspace's `fastModel` setting, expands it through `AiModelRegistryService.getEffectiveModelConfig`, validates it with `validateModelAvailability`, and stores the concrete model id — so the dropdown displays the model that will actually run, and workflow agents default to the cheaper fast tier instead of the smart one. Falls back to `AUTO_SELECT_FAST_MODEL_ID` if the lookup or validation fails (workspace missing, no AI provider configured, model disabled), so node creation never breaks. ### 2. Exclude `search_help_center` and `navigate_app` from workflow agent runs `ActionToolProvider` adds both tools unconditionally, but they only make sense in an interactive chat session (navigation targets the user's browser; help-center search is a support tool). They are now excluded via `WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES` in `AgentAsyncExecutorService`, alongside the existing output-navigation exclusions. Chat agents are unaffected. ## Test coverage - Existing specs for `WorkflowVersionStepOperationsWorkspaceService` and `AgentAsyncExecutorService` updated/passing (new constructor deps mocked). - `nx typecheck twenty-server` and `lint:diff-with-main` pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23447?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
import { type ActionToolId } from 'src/engine/core-modules/tool-provider/constants/action-tool-label.constant';
|
||||
|
||||
export const WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES = [
|
||||
'search_help_center',
|
||||
'navigate_app',
|
||||
] as const satisfies readonly ActionToolId[];
|
||||
+21
@@ -6,7 +6,9 @@ import { SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS } from 'twenty-shared/logic-funct
|
||||
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
@@ -34,6 +36,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
|
||||
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
|
||||
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
|
||||
let aiModelRegistryService: jest.Mocked<AiModelRegistryService>;
|
||||
let workspaceRepository: jest.Mocked<any>;
|
||||
|
||||
beforeEach(async () => {
|
||||
codeStepBuildService = {
|
||||
@@ -123,6 +127,15 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
flush: jest.fn(),
|
||||
} as unknown as jest.Mocked<WorkspaceCacheService>;
|
||||
|
||||
aiModelRegistryService = {
|
||||
getEffectiveModelConfig: jest.fn(),
|
||||
validateModelAvailability: jest.fn(),
|
||||
} as unknown as jest.Mocked<AiModelRegistryService>;
|
||||
|
||||
workspaceRepository = {
|
||||
findOneBy: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
WorkflowVersionStepOperationsWorkspaceService,
|
||||
@@ -150,6 +163,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
provide: getRepositoryToken(ObjectMetadataEntity),
|
||||
useValue: objectMetadataRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: workspaceRepository,
|
||||
},
|
||||
{
|
||||
provide: AiModelRegistryService,
|
||||
useValue: aiModelRegistryService,
|
||||
},
|
||||
{
|
||||
provide: WorkflowCommonWorkspaceService,
|
||||
useValue: workflowCommonWorkspaceService,
|
||||
|
||||
+28
-4
@@ -10,9 +10,9 @@ import {
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import {
|
||||
IF_ELSE_BRANCH_POSITION_OFFSETS,
|
||||
WorkflowActionType,
|
||||
getFunctionInputFromInputSchema,
|
||||
type StepIfElseBranch,
|
||||
WorkflowActionType,
|
||||
} from 'twenty-shared/workflow';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
@@ -20,14 +20,16 @@ import { v4 } from 'uuid';
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position.input';
|
||||
import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import {
|
||||
LogicFunctionException,
|
||||
LogicFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
|
||||
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
@@ -50,7 +52,6 @@ import {
|
||||
type WorkflowEmptyAction,
|
||||
type WorkflowFormAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
@@ -77,10 +78,13 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
|
||||
private readonly codeStepBuildService: CodeStepBuildService,
|
||||
private readonly agentService: AgentService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
@InjectWorkspaceScopedRepository(RoleTargetEntity)
|
||||
private readonly roleTargetRepository: WorkspaceScopedRepository<RoleTargetEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly aiAgentRoleService: AiAgentRoleService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
@@ -88,6 +92,26 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
private readonly workflowVersionCoreSyncService: WorkflowVersionCoreSyncService,
|
||||
) {}
|
||||
|
||||
private async getWorkspaceDefaultFastModelId(
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
`Workspace ${workspaceId} not found`,
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const effectiveModelConfig =
|
||||
this.aiModelRegistryService.getEffectiveModelConfig(workspace.fastModel);
|
||||
|
||||
return effectiveModelConfig.modelId;
|
||||
}
|
||||
|
||||
async runWorkflowVersionStepDeletionSideEffects({
|
||||
step,
|
||||
workspaceId,
|
||||
@@ -547,7 +571,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
description: '',
|
||||
prompt:
|
||||
'You are a helpful AI assistant. Complete the task based on the workflow context.',
|
||||
modelId: AUTO_SELECT_SMART_MODEL_ID,
|
||||
modelId: await this.getWorkspaceDefaultFastModelId(workspaceId),
|
||||
responseFormat: { type: 'text' },
|
||||
isCustom: true,
|
||||
},
|
||||
|
||||
+6
-1
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkflowVersionCoreModule } from 'src/engine/core-modules/workflow/workflow-version-core.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
@@ -29,7 +30,11 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
|
||||
AiAgentRoleModule,
|
||||
AiAgentModule,
|
||||
WorkspaceCacheModule,
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity, RoleTargetEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
ObjectMetadataEntity,
|
||||
RoleTargetEntity,
|
||||
WorkspaceEntity,
|
||||
]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkflowVersionCoreModule,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user