mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 18:49:20 +02:00
feat(core): Manual workflow executions call the unpublished agent (no-changelog) (#31585)
This commit is contained in:
parent
a3e37fcd12
commit
9cb9a1fc46
|
|
@ -19,6 +19,7 @@ import type {
|
|||
INodeExecutionData,
|
||||
INode,
|
||||
ITaskData,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
import { createRunExecutionData } from 'n8n-workflow';
|
||||
import type PCancelable from 'p-cancelable';
|
||||
|
|
@ -37,7 +38,6 @@ import { DataTableProxyService } from '@/modules/data-table/data-table-proxy.ser
|
|||
import { OwnershipService } from '@/services/ownership.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||
import { WorkflowHookContextService } from '@/workflow-hook-context.service';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import {
|
||||
executeAgent,
|
||||
|
|
@ -50,6 +50,7 @@ import {
|
|||
triggerReturnsLastRunOnly,
|
||||
} from '@/workflow-execute-additional-data';
|
||||
import * as WorkflowHelpers from '@/workflow-helpers';
|
||||
import { WorkflowHookContextService } from '@/workflow-hook-context.service';
|
||||
|
||||
const EXECUTION_ID = '123';
|
||||
const LAST_NODE_EXECUTED = 'Last node executed';
|
||||
|
|
@ -800,7 +801,7 @@ describe('WorkflowExecuteAdditionalData', () => {
|
|||
workflowId: 'workflow-1',
|
||||
});
|
||||
|
||||
await executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData);
|
||||
await executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData, 'manual');
|
||||
|
||||
expect(ownershipService.getWorkflowProjectCached).not.toHaveBeenCalled();
|
||||
expect(ownershipService.getPersonalProjectOwnerCached).not.toHaveBeenCalled();
|
||||
|
|
@ -812,6 +813,7 @@ describe('WorkflowExecuteAdditionalData', () => {
|
|||
'user-1',
|
||||
'project-1',
|
||||
'user-1',
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -828,7 +830,7 @@ describe('WorkflowExecuteAdditionalData', () => {
|
|||
mock<User>({ id: 'owner-1' }),
|
||||
);
|
||||
|
||||
await executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData);
|
||||
await executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData, 'manual');
|
||||
|
||||
expect(ownershipService.getWorkflowProjectCached).toHaveBeenCalledWith('workflow-1');
|
||||
expect(ownershipService.getPersonalProjectOwnerCached).toHaveBeenCalledWith('project-1');
|
||||
|
|
@ -840,6 +842,7 @@ describe('WorkflowExecuteAdditionalData', () => {
|
|||
'owner-1',
|
||||
'project-1',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -855,7 +858,7 @@ describe('WorkflowExecuteAdditionalData', () => {
|
|||
ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData),
|
||||
executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData, 'manual'),
|
||||
).rejects.toThrow('Cannot execute agent without a userId in additional data');
|
||||
expect(agentsService.executeForWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -868,10 +871,67 @@ describe('WorkflowExecuteAdditionalData', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData),
|
||||
executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData, 'manual'),
|
||||
).rejects.toThrow('Cannot execute agent without a userId in additional data');
|
||||
expect(ownershipService.getWorkflowProjectCached).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each<WorkflowExecuteMode>(['manual', 'chat'])(
|
||||
'runs draft agent for %s executions',
|
||||
async (mode) => {
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
workflowId: 'workflow-1',
|
||||
});
|
||||
|
||||
await executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData, mode);
|
||||
|
||||
// agentsService.executeForWorkflow should with 8th parameter true
|
||||
expect(agentsService.executeForWorkflow).toHaveBeenCalledWith(
|
||||
AGENT_ID,
|
||||
MESSAGE,
|
||||
EXEC_ID,
|
||||
THREAD_ID,
|
||||
'user-1',
|
||||
'project-1',
|
||||
'user-1',
|
||||
true,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each<WorkflowExecuteMode>([
|
||||
'cli',
|
||||
'error',
|
||||
'integrated',
|
||||
'internal',
|
||||
'retry',
|
||||
'trigger',
|
||||
'webhook',
|
||||
'evaluation',
|
||||
'agent',
|
||||
])('runs published agent for %s executions', async (mode) => {
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
userId: 'user-1',
|
||||
projectId: 'project-1',
|
||||
workflowId: 'workflow-1',
|
||||
});
|
||||
|
||||
await executeAgent(AGENT_ID, MESSAGE, EXEC_ID, THREAD_ID, additionalData, mode);
|
||||
|
||||
// agentsService.executeForWorkflow should with 8th parameter true
|
||||
expect(agentsService.executeForWorkflow).toHaveBeenCalledWith(
|
||||
AGENT_ID,
|
||||
MESSAGE,
|
||||
EXEC_ID,
|
||||
THREAD_ID,
|
||||
'user-1',
|
||||
'project-1',
|
||||
'user-1',
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSubWorkflowOutput', () => {
|
||||
|
|
|
|||
|
|
@ -911,16 +911,7 @@ export class AgentsService {
|
|||
let agentData: Agent = agentEntity;
|
||||
|
||||
if (usePublishedVersion) {
|
||||
const activeVersionSchema = agentEntity.activeVersion?.schema;
|
||||
if (!activeVersionSchema) {
|
||||
throw new NotFoundError(`Agent ${agentId} is not published`);
|
||||
}
|
||||
agentData = {
|
||||
...agentEntity,
|
||||
schema: activeVersionSchema,
|
||||
tools: agentEntity.activeVersion?.tools ?? agentEntity.tools ?? {},
|
||||
skills: agentEntity.activeVersion?.skills ?? agentEntity.skills ?? {},
|
||||
} as Agent;
|
||||
agentData = this.getPublishedAgent(agentEntity);
|
||||
|
||||
// Resolve n8n user from publishedById when not provided by the caller.
|
||||
n8nUserId ??= agentEntity.activeVersion?.publishedById ?? undefined;
|
||||
|
|
@ -1622,6 +1613,22 @@ export class AgentsService {
|
|||
}
|
||||
}
|
||||
|
||||
private getPublishedAgent(agentEntity: Agent): Agent {
|
||||
const activeVersionSchema = agentEntity.activeVersion?.schema;
|
||||
if (!activeVersionSchema) {
|
||||
throw new OperationalError(
|
||||
'Agent is not published. Publish the agent before using it in a workflow.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...agentEntity,
|
||||
schema: activeVersionSchema,
|
||||
tools: agentEntity.activeVersion?.tools ?? agentEntity.tools ?? {},
|
||||
skills: agentEntity.activeVersion?.skills ?? agentEntity.skills ?? {},
|
||||
} as Agent;
|
||||
}
|
||||
|
||||
async executeForWorkflow(
|
||||
agentId: string,
|
||||
message: string,
|
||||
|
|
@ -1630,24 +1637,25 @@ export class AgentsService {
|
|||
userId: string,
|
||||
projectId: string,
|
||||
telemetryUserId?: string,
|
||||
useDraftVersion?: boolean,
|
||||
): Promise<ExecuteAgentData> {
|
||||
const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
|
||||
if (!agentEntity) {
|
||||
throw new OperationalError('Agent not found or not accessible.');
|
||||
}
|
||||
|
||||
if (!agentEntity.activeVersionId) {
|
||||
throw new OperationalError(
|
||||
'Agent is not published. Publish the agent before using it in a workflow.',
|
||||
);
|
||||
}
|
||||
|
||||
const credentialProvider = new AgentsCredentialProvider(
|
||||
Container.get(CredentialsService),
|
||||
projectId,
|
||||
);
|
||||
|
||||
const compiled = await this.compileIsolated(agentEntity, credentialProvider, userId);
|
||||
let agentData: Agent = agentEntity;
|
||||
|
||||
if (!useDraftVersion) {
|
||||
agentData = this.getPublishedAgent(agentEntity);
|
||||
}
|
||||
|
||||
const compiled = await this.compileIsolated(agentData, credentialProvider, userId);
|
||||
if (!compiled.ok || !compiled.agent) {
|
||||
throw new OperationalError(`Failed to compile agent: ${compiled.error ?? 'unknown error'}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import type { PushMessage, PushType } from '@n8n/api-types';
|
||||
import { Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||
import { GlobalConfig, SsrfProtectionConfig } from '@n8n/config';
|
||||
|
|
@ -46,8 +44,8 @@ import { CredentialsHelper } from '@/credentials-helper';
|
|||
import { EventService } from '@/events/event.service';
|
||||
import type { AiEventPayload } from '@/events/maps/ai.event-map';
|
||||
import { getLifecycleHooksForSubExecutions } from '@/execution-lifecycle/execution-lifecycle-hooks';
|
||||
import { FailedRunFactory } from '@/executions/failed-run-factory';
|
||||
import { isManualOrChatExecution } from '@/executions/execution.utils';
|
||||
import { FailedRunFactory } from '@/executions/failed-run-factory';
|
||||
import {
|
||||
CredentialsPermissionChecker,
|
||||
SubworkflowPolicyChecker,
|
||||
|
|
@ -55,12 +53,13 @@ import {
|
|||
import type { UpdateExecutionPayload } from '@/interfaces';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { Push } from '@/push';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { SsrfProtectionService } from '@/services/ssrf/ssrf-protection.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { TaskRequester } from '@/task-runners/task-managers/task-requester';
|
||||
import { findSubworkflowStart } from '@/utils';
|
||||
import { objectToError } from '@/utils/object-to-error';
|
||||
import * as WorkflowHelpers from '@/workflow-helpers';
|
||||
|
||||
import { RuntimeCredentialProxyService } from './services/runtime-credential-proxy.service';
|
||||
|
||||
export function getRunData(
|
||||
|
|
@ -267,6 +266,7 @@ export async function executeAgent(
|
|||
executionId: string,
|
||||
threadId: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
executionMode: WorkflowExecuteMode,
|
||||
): Promise<ExecuteAgentData> {
|
||||
let userId = additionalData.userId;
|
||||
const telemetryUserId = additionalData.userId;
|
||||
|
|
@ -300,6 +300,8 @@ export async function executeAgent(
|
|||
const { AgentsService } = await import('@/modules/agents/agents.service');
|
||||
const agentsService = Container.get(AgentsService);
|
||||
|
||||
const useDraftVersion = isManualOrChatExecution(executionMode);
|
||||
|
||||
return await agentsService.executeForWorkflow(
|
||||
agentId,
|
||||
message,
|
||||
|
|
@ -308,18 +310,20 @@ export async function executeAgent(
|
|||
userId,
|
||||
projectId,
|
||||
telemetryUserId,
|
||||
useDraftVersion,
|
||||
);
|
||||
}
|
||||
|
||||
async function listAgents(userId: string): Promise<Array<{ id: string; name: string }>> {
|
||||
const { AgentsService } = await import('@/modules/agents/agents.service');
|
||||
const agentsService = Container.get(AgentsService);
|
||||
// Only published agents are runnable from a workflow — see the publish
|
||||
// guard in `executeForWorkflow`. Filtering here keeps unpublished agents
|
||||
// out of the MessageAnAgent dropdown so users don't pick one that would
|
||||
// fail at execution time.
|
||||
const agents = await agentsService.findPublishedByUser(userId);
|
||||
return agents.map((agent) => ({ id: agent.id, name: agent.name }));
|
||||
// Only published agents are runnable from a published workflow.
|
||||
// But unpublished agents may be called from manual workflow executions (e.g. during development), so they are included in the list as well.
|
||||
const agents = await agentsService.findByUser(userId);
|
||||
return agents.map((agent) => ({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ export class BaseExecuteContext extends NodeExecutionContext {
|
|||
executionId,
|
||||
threadId,
|
||||
this.additionalData,
|
||||
this.additionalData.rootExecutionMode ?? this.getMode(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
|||
if (!this.additionalData.listAgents || !this.additionalData.userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.additionalData.listAgents(this.additionalData.userId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,13 +31,6 @@ export class MessageAnAgent implements INodeType {
|
|||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'Create an n8n agent <a href="/new-agent" target="_blank">here</a>. Only published agents are listed below.',
|
||||
name: 'publishedAgentNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Agent',
|
||||
name: 'agentId',
|
||||
|
|
|
|||
|
|
@ -3247,6 +3247,7 @@ export interface IWorkflowExecuteAdditionalData {
|
|||
executionId: string,
|
||||
threadId: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
executionMode: WorkflowExecuteMode,
|
||||
) => Promise<ExecuteAgentData>;
|
||||
listAgents?: (userId: string) => Promise<Array<{ id: string; name: string }>>;
|
||||
getRunExecutionData: (executionId: string) => Promise<IRunExecutionData | undefined>;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user