Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1419-…
Browse files Browse the repository at this point in the history
…notion-node-database-page-param-cant-parse-pip-links
  • Loading branch information
michael-radency committed Nov 11, 2024
2 parents 8fac092 + af7d6e6 commit 7df2300
Show file tree
Hide file tree
Showing 16 changed files with 334 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
import { getTracingConfig } from '../../../../../utils/tracing';
import { extractParsedOutput } from '../utils';
import { checkForStructuredTools, extractParsedOutput } from '../utils';

export async function conversationalAgentExecute(
this: IExecuteFunctions,
Expand All @@ -34,6 +34,8 @@ export async function conversationalAgentExecute(
const tools = await getConnectedTools(this, nodeVersion >= 1.5);
const outputParsers = await getOptionalOutputParsers(this);

await checkForStructuredTools(tools, this.getNode(), 'Conversational Agent');

// TODO: Make it possible in the future to use values for other items than just 0
const options = this.getNodeParameter('options', 0, {}) as {
systemMessage?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getConnectedTools, getPromptInputByType } from '../../../../../utils/he
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
import { getTracingConfig } from '../../../../../utils/tracing';
import { extractParsedOutput } from '../utils';
import { checkForStructuredTools, extractParsedOutput } from '../utils';

export async function planAndExecuteAgentExecute(
this: IExecuteFunctions,
Expand All @@ -28,6 +28,7 @@ export async function planAndExecuteAgentExecute(

const tools = await getConnectedTools(this, nodeVersion >= 1.5);

await checkForStructuredTools(tools, this.getNode(), 'Plan & Execute Agent');
const outputParsers = await getOptionalOutputParsers(this);

const options = this.getNodeParameter('options', 0, {}) as {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
import { getTracingConfig } from '../../../../../utils/tracing';
import { extractParsedOutput } from '../utils';
import { checkForStructuredTools, extractParsedOutput } from '../utils';

export async function reActAgentAgentExecute(
this: IExecuteFunctions,
Expand All @@ -33,6 +33,8 @@ export async function reActAgentAgentExecute(

const tools = await getConnectedTools(this, nodeVersion >= 1.5);

await checkForStructuredTools(tools, this.getNode(), 'ReAct Agent');

const outputParsers = await getOptionalOutputParsers(this);

const options = this.getNodeParameter('options', 0, {}) as {
Expand Down
25 changes: 24 additions & 1 deletion packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ZodObjectAny } from '@langchain/core/dist/types/zod';
import type { BaseOutputParser } from '@langchain/core/output_parsers';
import type { IExecuteFunctions } from 'n8n-workflow';
import type { DynamicStructuredTool, Tool } from 'langchain/tools';
import { NodeOperationError, type IExecuteFunctions, type INode } from 'n8n-workflow';

export async function extractParsedOutput(
ctx: IExecuteFunctions,
Expand All @@ -17,3 +19,24 @@ export async function extractParsedOutput(
// with fallback to the original output if it's not present
return parsedOutput?.output ?? parsedOutput;
}

export async function checkForStructuredTools(
tools: Array<Tool | DynamicStructuredTool<ZodObjectAny>>,
node: INode,
currentAgentType: string,
) {
const dynamicStructuredTools = tools.filter(
(tool) => tool.constructor.name === 'DynamicStructuredTool',
);
if (dynamicStructuredTools.length > 0) {
const getToolName = (tool: Tool | DynamicStructuredTool) => `"${tool.name}"`;
throw new NodeOperationError(
node,
`The selected tools are not supported by "${currentAgentType}", please use "Tools Agent" instead`,
{
itemIndex: 0,
description: `Incompatible connected tools: ${dynamicStructuredTools.map(getToolName).join(', ')}`,
},
);
}
}
106 changes: 106 additions & 0 deletions packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Tool } from 'langchain/tools';
import { DynamicStructuredTool } from 'langchain/tools';
import { NodeOperationError } from 'n8n-workflow';
import type { INode } from 'n8n-workflow';
import { z } from 'zod';

import { checkForStructuredTools } from '../agents/utils';

describe('checkForStructuredTools', () => {
let mockNode: INode;

beforeEach(() => {
mockNode = {
id: 'test-node',
name: 'Test Node',
type: 'test',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
});

it('should not throw error when no DynamicStructuredTools are present', async () => {
const tools = [
{
name: 'regular-tool',
constructor: { name: 'Tool' },
} as Tool,
];

await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).resolves.not.toThrow();
});

it('should throw NodeOperationError when DynamicStructuredTools are present', async () => {
const dynamicTool = new DynamicStructuredTool({
name: 'dynamic-tool',
description: 'test tool',
schema: z.object({}),
func: async () => 'result',
});

const tools: Array<Tool | DynamicStructuredTool> = [dynamicTool];

await expect(checkForStructuredTools(tools, mockNode, 'Conversation Agent')).rejects.toThrow(
NodeOperationError,
);

await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).rejects.toMatchObject({
message:
'The selected tools are not supported by "Conversation Agent", please use "Tools Agent" instead',
description: 'Incompatible connected tools: "dynamic-tool"',
});
});

it('should list multiple dynamic tools in error message', async () => {
const dynamicTool1 = new DynamicStructuredTool({
name: 'dynamic-tool-1',
description: 'test tool 1',
schema: z.object({}),
func: async () => 'result',
});

const dynamicTool2 = new DynamicStructuredTool({
name: 'dynamic-tool-2',
description: 'test tool 2',
schema: z.object({}),
func: async () => 'result',
});

const tools = [dynamicTool1, dynamicTool2];

await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).rejects.toMatchObject({
description: 'Incompatible connected tools: "dynamic-tool-1", "dynamic-tool-2"',
});
});

it('should throw error with mixed tool types and list only dynamic tools in error message', async () => {
const regularTool = {
name: 'regular-tool',
constructor: { name: 'Tool' },
} as Tool;

const dynamicTool = new DynamicStructuredTool({
name: 'dynamic-tool',
description: 'test tool',
schema: z.object({}),
func: async () => 'result',
});

const tools = [regularTool, dynamicTool];

await expect(
checkForStructuredTools(tools, mockNode, 'Conversation Agent'),
).rejects.toMatchObject({
message:
'The selected tools are not supported by "Conversation Agent", please use "Tools Agent" instead',
description: 'Incompatible connected tools: "dynamic-tool"',
});
});
});
20 changes: 19 additions & 1 deletion packages/@n8n/task-runner/src/task-runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApplicationError } from 'n8n-workflow';
import { ApplicationError, ensureError } from 'n8n-workflow';
import { nanoid } from 'nanoid';
import { type MessageEvent, WebSocket } from 'ws';

Expand Down Expand Up @@ -88,6 +88,24 @@ export abstract class TaskRunner {
},
maxPayload: opts.maxPayloadSize,
});

this.ws.addEventListener('error', (event) => {
const error = ensureError(event.error);

if (
'code' in error &&
typeof error.code === 'string' &&
['ECONNREFUSED', 'ENOTFOUND'].some((code) => code === error.code)
) {
console.error(
`Error: Failed to connect to n8n. Please ensure n8n is reachable at: ${opts.n8nUri}`,
);
process.exit(1);
} else {
console.error(`Error: Failed to connect to n8n at ${opts.n8nUri}`);
console.error('Details:', event.message || 'Unknown error');
}
});
this.ws.addEventListener('message', this.receiveMessage);
this.ws.addEventListener('close', this.stopTaskOffers);
}
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/load-nodes-and-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,15 @@ export class LoadNodesAndCredentials {
const toWatch = loader.isLazyLoaded
? ['**/nodes.json', '**/credentials.json']
: ['**/*.js', '**/*.json'];
watch(toWatch, { cwd: realModulePath }).on('change', reloader);
const files = await glob(toWatch, {
cwd: realModulePath,
ignore: ['node_modules/**'],
});
const watcher = watch(files, {
cwd: realModulePath,
ignoreInitial: true,
});
watcher.on('add', reloader).on('change', reloader).on('unlink', reloader);
});
}
}
16 changes: 10 additions & 6 deletions packages/cli/src/runners/errors/task-runner-oom-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import type { TaskRunner } from '../task-broker.service';
export class TaskRunnerOomError extends ApplicationError {
public description: string;

constructor(runnerId: TaskRunner['id'], isCloudDeployment: boolean) {
super(`Task runner (${runnerId}) ran out of memory.`, { level: 'error' });
constructor(
public readonly runnerId: TaskRunner['id'],
isCloudDeployment: boolean,
) {
super('Node ran out of memory.', { level: 'error' });

const fixSuggestions = {
reduceItems: 'Reduce the number of items processed at a time by batching the input.',
reduceItems:
'Reduce the number of items processed at a time, by batching them using a loop node',
increaseMemory:
"Increase the memory available to the task runner with 'N8N_RUNNERS_MAX_OLD_SPACE_SIZE' environment variable.",
upgradePlan: 'Upgrade your cloud plan to increase the available memory.',
"Increase the memory available to the task runner with 'N8N_RUNNERS_MAX_OLD_SPACE_SIZE' environment variable",
upgradePlan: 'Upgrade your cloud plan to increase the available memory',
};

const subtitle =
'The runner executing the code ran out of memory. This usually happens when there are too many items to process. You can try the following:';
'This usually happens when there are too many items to process. You can try the following:';
const suggestions = isCloudDeployment
? [fixSuggestions.reduceItems, fixSuggestions.upgradePlan]
: [fixSuggestions.reduceItems, fixSuggestions.increaseMemory];
Expand Down
1 change: 0 additions & 1 deletion packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,6 @@ export type WorkflowCallerPolicyDefaultOption = 'any' | 'none' | 'workflowsFromA

export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
errorWorkflow?: string;
saveManualExecutions?: boolean;
timezone?: string;
executionTimeout?: number;
maxExecutionTimeout?: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/editor-ui/src/__tests__/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export const defaultSettings: FrontendSettings = {
},
publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } },
pushBackend: 'websocket',
saveDataErrorExecution: 'DEFAULT',
saveDataSuccessExecution: 'DEFAULT',
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: false,
saveExecutionProgress: false,
sso: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const isDeleting = ref(false);
const isSaving = ref(false);
const isTesting = ref(false);
const hasUnsavedChanges = ref(false);
const isSaved = ref(false);
const loading = ref(false);
const showValidationWarning = ref(false);
const testedSuccessfully = ref(false);
Expand Down Expand Up @@ -317,8 +318,8 @@ const defaultCredentialTypeName = computed(() => {
const showSaveButton = computed(() => {
return (
(hasUnsavedChanges.value || !!credentialId.value) &&
(credentialPermissions.value.create || credentialPermissions.value.update)
(props.mode === 'new' || hasUnsavedChanges.value || isSaved.value) &&
(credentialPermissions.value.create ?? credentialPermissions.value.update)
);
});
Expand Down Expand Up @@ -838,6 +839,7 @@ async function updateCredential(
isSharedWithChanged.value = false;
}
hasUnsavedChanges.value = false;
isSaved.value = true;
if (credential) {
await externalHooks.run('credential.saved', {
Expand Down Expand Up @@ -889,6 +891,7 @@ async function deleteCredential() {
isDeleting.value = true;
await credentialsStore.deleteCredential({ id: credentialId.value });
hasUnsavedChanges.value = false;
isSaved.value = true;
} catch (error) {
toast.showError(
error,
Expand Down Expand Up @@ -1074,7 +1077,7 @@ function resetCredentialData(): void {
/>
<SaveButton
v-if="showSaveButton"
:saved="!hasUnsavedChanges && !isTesting"
:saved="!hasUnsavedChanges && !isTesting && !!credentialId"
:is-saving="isSaving || isTesting"
:saving-label="
isTesting
Expand Down
Loading

0 comments on commit 7df2300

Please sign in to comment.