mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-29 15:57:00 +02:00
Co-authored-by: Matsu <matias.huhta@n8n.io> Co-authored-by: Dawid Myslak <dawid.myslak@gmail.com> Co-authored-by: Bernhard Wittmann <bernhard.wittmann@n8n.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com> Co-authored-by: Benjamin Schroth <68321970+schrothbn@users.noreply.github.com> Co-authored-by: Danny Martini <danny@n8n.io> Co-authored-by: RomanDavydchuk <roman.davydchuk@n8n.io> Co-authored-by: Sandra Zollner <sandra.zollner@n8n.io> Co-authored-by: Milorad FIlipović <milorad@n8n.io> Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
471 lines
14 KiB
TypeScript
471 lines
14 KiB
TypeScript
import type { Tool } from '@langchain/core/tools';
|
|
import { makeResolverFromLegacyOptions } from 'vm2';
|
|
import { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox';
|
|
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
|
import { standardizeOutput } from 'n8n-nodes-base/dist/nodes/Code/utils';
|
|
import { NodeOperationError, NodeConnectionTypes, LOG_LEVELS } from 'n8n-workflow';
|
|
import type {
|
|
IExecuteFunctions,
|
|
INodeExecutionData,
|
|
INodeType,
|
|
INodeTypeDescription,
|
|
INodeOutputConfiguration,
|
|
SupplyData,
|
|
ISupplyDataFunctions,
|
|
Logger,
|
|
} from 'n8n-workflow';
|
|
|
|
// TODO: Add support for execute function. Got already started but got commented out
|
|
|
|
import { logWrapper } from '@n8n/ai-utilities';
|
|
|
|
const { NODE_FUNCTION_ALLOW_BUILTIN: builtIn, NODE_FUNCTION_ALLOW_EXTERNAL: external } =
|
|
process.env;
|
|
|
|
// TODO: Replace
|
|
const connectorTypes = {
|
|
[NodeConnectionTypes.AiChain]: 'Chain',
|
|
[NodeConnectionTypes.AiDocument]: 'Document',
|
|
[NodeConnectionTypes.AiEmbedding]: 'Embedding',
|
|
[NodeConnectionTypes.AiLanguageModel]: 'Language Model',
|
|
[NodeConnectionTypes.AiMemory]: 'Memory',
|
|
[NodeConnectionTypes.AiOutputParser]: 'Output Parser',
|
|
[NodeConnectionTypes.AiTextSplitter]: 'Text Splitter',
|
|
[NodeConnectionTypes.AiTool]: 'Tool',
|
|
[NodeConnectionTypes.AiVectorStore]: 'Vector Store',
|
|
[NodeConnectionTypes.Main]: 'Main',
|
|
};
|
|
|
|
const defaultCodeExecute = `const { PromptTemplate } = require('@langchain/core/prompts');
|
|
|
|
const query = 'Tell me a joke';
|
|
const prompt = PromptTemplate.fromTemplate(query);
|
|
|
|
// If you are allowing more than one language model input connection (-1 or
|
|
// anything greater than 1), getInputConnectionData returns an array, so you
|
|
// will have to change the code below it to deal with that. For example, use
|
|
// llm[0] in the chain definition
|
|
|
|
const llm = await this.getInputConnectionData('ai_languageModel', 0);
|
|
let chain = prompt.pipe(llm);
|
|
const output = await chain.invoke();
|
|
return [ {json: { output } } ];
|
|
|
|
// NOTE: Old langchain imports (e.g., 'langchain/chains') are automatically
|
|
// converted to '@langchain/classic' imports for backwards compatibility.`;
|
|
|
|
const defaultCodeSupplyData = `const { WikipediaQueryRun } = require( '@langchain/community/tools/wikipedia_query_run');
|
|
return new WikipediaQueryRun();`;
|
|
|
|
/**
|
|
* Transforms old langchain import paths to @langchain/classic for backwards compatibility.
|
|
* Only transforms paths that actually moved to the classic package.
|
|
*
|
|
* @param moduleName - The original module name from the import statement
|
|
* @returns The transformed module name, or the original if no transformation is needed
|
|
*/
|
|
export function transformLegacyLangchainImport(moduleName: string): string {
|
|
// List of langchain submodules that moved to @langchain/classic
|
|
// Based on https://www.npmjs.com/package/@langchain/classic exports
|
|
const classicModules = [
|
|
'agents',
|
|
'callbacks',
|
|
'chains',
|
|
'chat_models/universal',
|
|
'document',
|
|
'document_loaders',
|
|
'document_transformers',
|
|
'embeddings/cache_backed',
|
|
'embeddings/fake',
|
|
'evaluation',
|
|
'experimental',
|
|
'hub',
|
|
'indexes',
|
|
'load',
|
|
'memory',
|
|
'output_parsers',
|
|
'retrievers',
|
|
'schema',
|
|
'smith',
|
|
'sql_db',
|
|
'storage',
|
|
'stores',
|
|
'text_splitter',
|
|
'tools',
|
|
'util',
|
|
'vectorstores',
|
|
];
|
|
|
|
// Check if this is a langchain/ import (old style)
|
|
if (moduleName.startsWith('langchain/')) {
|
|
const subpath = moduleName.substring('langchain/'.length);
|
|
|
|
// Check if this subpath or any parent path is in the classic modules list
|
|
for (const classicModule of classicModules) {
|
|
if (subpath === classicModule || subpath.startsWith(classicModule + '/')) {
|
|
// Transform to @langchain/classic
|
|
return `@langchain/classic/${subpath}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return moduleName;
|
|
}
|
|
|
|
/**
|
|
* Transforms user code to replace old langchain require/import statements
|
|
* with @langchain/classic equivalents.
|
|
*
|
|
* @param code - The user's code string
|
|
* @returns The transformed code with updated import paths
|
|
*/
|
|
function transformLegacyLangchainCode(code: string): string {
|
|
// Transform require statements: require('langchain/...')
|
|
let transformedCode = code.replace(
|
|
/require\s*\(\s*['"]langchain\/([\w/_]+)['"]\s*\)/g,
|
|
(match, subpath) => {
|
|
const oldPath = `langchain/${subpath}`;
|
|
const newPath = transformLegacyLangchainImport(oldPath);
|
|
return newPath === oldPath ? match : `require('${newPath}')`;
|
|
},
|
|
);
|
|
|
|
// Transform import statements: from 'langchain/...'
|
|
transformedCode = transformedCode.replace(
|
|
/from\s+['"]langchain\/([\w/_]+)['"]/g,
|
|
(match, subpath) => {
|
|
const oldPath = `langchain/${subpath}`;
|
|
const newPath = transformLegacyLangchainImport(oldPath);
|
|
return newPath === oldPath ? match : `from '${newPath}'`;
|
|
},
|
|
);
|
|
|
|
return transformedCode;
|
|
}
|
|
|
|
function isLoggerKey(level: string): level is keyof Logger {
|
|
return level !== 'silent';
|
|
}
|
|
|
|
export function createSandboxLogger(logger: Logger): Logger {
|
|
return LOG_LEVELS.filter(isLoggerKey).reduce((result, level) => {
|
|
result[level] = (...args: Parameters<Logger[keyof Logger]>) =>
|
|
logger[level].apply(logger, args);
|
|
return result;
|
|
}, {} as Logger);
|
|
}
|
|
|
|
const langchainModules = ['langchain', '@langchain/*'];
|
|
export const vmResolver = makeResolverFromLegacyOptions({
|
|
external: {
|
|
modules: external ? [...langchainModules, ...external.split(',')] : [...langchainModules],
|
|
transitive: false,
|
|
},
|
|
resolve(moduleName, parentDirname) {
|
|
if (moduleName.match(/^langchain\//) ?? moduleName.match(/^@langchain\//)) {
|
|
return require.resolve(`@n8n/n8n-nodes-langchain/node_modules/${moduleName}.cjs`, {
|
|
paths: [parentDirname],
|
|
});
|
|
}
|
|
return;
|
|
},
|
|
builtin: builtIn?.split(',') ?? [],
|
|
});
|
|
|
|
function getSandbox(
|
|
this: IExecuteFunctions | ISupplyDataFunctions,
|
|
code: string,
|
|
options?: { addItems?: boolean; itemIndex?: number },
|
|
) {
|
|
const itemIndex = options?.itemIndex ?? 0;
|
|
const node = this.getNode();
|
|
const workflowMode = this.getMode();
|
|
|
|
// Transform legacy langchain imports to @langchain/classic
|
|
const transformedCode = transformLegacyLangchainCode(code);
|
|
|
|
const context = getSandboxContext.call(this, itemIndex);
|
|
context.addInputData = this.addInputData.bind(this);
|
|
context.addOutputData = this.addOutputData.bind(this);
|
|
context.getInputConnectionData = this.getInputConnectionData.bind(this);
|
|
context.getInputData = this.getInputData.bind(this);
|
|
context.getNode = this.getNode.bind(this);
|
|
context.getExecutionCancelSignal = this.getExecutionCancelSignal.bind(this);
|
|
context.getNodeOutputs = this.getNodeOutputs.bind(this);
|
|
context.executeWorkflow = this.executeWorkflow.bind(this);
|
|
context.getWorkflowDataProxy = this.getWorkflowDataProxy.bind(this);
|
|
context.logger = createSandboxLogger(this.logger);
|
|
|
|
if (options?.addItems) {
|
|
context.items = context.$input.all();
|
|
}
|
|
|
|
const sandbox = new JavaScriptSandbox(context, transformedCode, this.helpers, {
|
|
resolver: vmResolver,
|
|
});
|
|
|
|
sandbox.on(
|
|
'output',
|
|
workflowMode === 'manual'
|
|
? this.sendMessageToUI.bind(this)
|
|
: (...args: unknown[]) =>
|
|
console.log(`[Workflow "${this.getWorkflow().id}"][Node "${node.name}"]`, ...args),
|
|
);
|
|
return sandbox;
|
|
}
|
|
|
|
export class Code implements INodeType {
|
|
description: INodeTypeDescription = {
|
|
displayName: 'LangChain Code',
|
|
name: 'code',
|
|
icon: 'fa:code',
|
|
iconColor: 'black',
|
|
group: ['transform'],
|
|
version: 1,
|
|
description: 'LangChain Code Node',
|
|
defaults: {
|
|
name: 'LangChain Code',
|
|
},
|
|
codex: {
|
|
categories: ['AI'],
|
|
subcategories: {
|
|
AI: ['Miscellaneous'],
|
|
},
|
|
resources: {
|
|
primaryDocumentation: [
|
|
{
|
|
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.code/',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
inputs: `={{ ((values) => { const connectorTypes = ${JSON.stringify(
|
|
connectorTypes,
|
|
)}; return values.map(value => { return { type: value.type, required: value.required, maxConnections: value.maxConnections === -1 ? undefined : value.maxConnections, displayName: connectorTypes[value.type] !== 'Main' ? connectorTypes[value.type] : undefined } } ) })($parameter.inputs.input) }}`,
|
|
outputs: `={{ ((values) => { const connectorTypes = ${JSON.stringify(
|
|
connectorTypes,
|
|
)}; return values.map(value => { return { type: value.type, displayName: connectorTypes[value.type] !== 'Main' ? connectorTypes[value.type] : undefined } } ) })($parameter.outputs.output) }}`,
|
|
properties: [
|
|
{
|
|
displayName: 'Code',
|
|
name: 'code',
|
|
placeholder: 'Add Code',
|
|
type: 'fixedCollection',
|
|
noDataExpression: true,
|
|
default: {},
|
|
options: [
|
|
{
|
|
name: 'execute',
|
|
displayName: 'Execute',
|
|
values: [
|
|
{
|
|
displayName: 'JavaScript - Execute',
|
|
name: 'code',
|
|
type: 'string',
|
|
typeOptions: {
|
|
editor: 'jsEditor',
|
|
},
|
|
default: defaultCodeExecute,
|
|
hint: 'This code will only run and return data if a "Main" input & output got created.',
|
|
noDataExpression: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'supplyData',
|
|
displayName: 'Supply Data',
|
|
values: [
|
|
{
|
|
displayName: 'JavaScript - Supply Data',
|
|
name: 'code',
|
|
type: 'string',
|
|
typeOptions: {
|
|
editor: 'jsEditor',
|
|
},
|
|
default: defaultCodeSupplyData,
|
|
hint: 'This code will only run and return data if an output got created which is not "Main".',
|
|
noDataExpression: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// TODO: Add links to docs which provide additional information regarding functionality
|
|
{
|
|
displayName:
|
|
'You can import LangChain and use all available functionality. Debug by using <code>console.log()</code> statements and viewing their output in the browser console.',
|
|
name: 'notice',
|
|
type: 'notice',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Inputs',
|
|
name: 'inputs',
|
|
placeholder: 'Add Input',
|
|
type: 'fixedCollection',
|
|
noDataExpression: true,
|
|
typeOptions: {
|
|
multipleValues: true,
|
|
sortable: true,
|
|
},
|
|
description: 'The input to add',
|
|
default: {},
|
|
options: [
|
|
{
|
|
name: 'input',
|
|
displayName: 'Input',
|
|
values: [
|
|
{
|
|
displayName: 'Type',
|
|
name: 'type',
|
|
type: 'options',
|
|
options: Object.keys(connectorTypes).map((key) => ({
|
|
name: connectorTypes[key as keyof typeof connectorTypes],
|
|
value: key,
|
|
})),
|
|
noDataExpression: true,
|
|
default: '',
|
|
required: true,
|
|
description: 'The type of the input',
|
|
},
|
|
{
|
|
displayName: 'Max Connections',
|
|
name: 'maxConnections',
|
|
type: 'number',
|
|
noDataExpression: true,
|
|
default: -1,
|
|
required: true,
|
|
description:
|
|
'How many nodes of this type are allowed to be connected. Set it to -1 for unlimited.',
|
|
},
|
|
{
|
|
displayName: 'Required',
|
|
name: 'required',
|
|
type: 'boolean',
|
|
noDataExpression: true,
|
|
default: false,
|
|
required: true,
|
|
description: 'Whether the input needs a connection',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Outputs',
|
|
name: 'outputs',
|
|
placeholder: 'Add Output',
|
|
type: 'fixedCollection',
|
|
noDataExpression: true,
|
|
typeOptions: {
|
|
multipleValues: true,
|
|
sortable: true,
|
|
},
|
|
description: 'The output to add',
|
|
default: {},
|
|
options: [
|
|
{
|
|
name: 'output',
|
|
displayName: 'Output',
|
|
values: [
|
|
{
|
|
displayName: 'Type',
|
|
name: 'type',
|
|
type: 'options',
|
|
options: Object.keys(connectorTypes).map((key) => ({
|
|
name: connectorTypes[key as keyof typeof connectorTypes],
|
|
value: key,
|
|
})),
|
|
noDataExpression: true,
|
|
default: '',
|
|
required: true,
|
|
description: 'The type of the input',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
const itemIndex = 0;
|
|
|
|
const code = this.getNodeParameter('code', itemIndex) as { execute?: { code: string } };
|
|
|
|
if (!code.execute?.code) {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
`No code for "Execute" set on node "${this.getNode().name}`,
|
|
{
|
|
itemIndex,
|
|
},
|
|
);
|
|
}
|
|
|
|
const sandbox = getSandbox.call(this, code.execute.code, { addItems: true, itemIndex });
|
|
|
|
const outputs = this.getNodeOutputs();
|
|
const mainOutputs: INodeOutputConfiguration[] = outputs.filter(
|
|
(output) => output.type === NodeConnectionTypes.Main,
|
|
);
|
|
|
|
const options = { multiOutput: mainOutputs.length !== 1 };
|
|
|
|
let items: INodeExecutionData[] | INodeExecutionData[][];
|
|
try {
|
|
items = await sandbox.runCodeAllItems(options);
|
|
} catch (error) {
|
|
if (!this.continueOnFail()) throw error;
|
|
items = [{ json: { error: (error as Error).message } }];
|
|
if (options.multiOutput) {
|
|
items = [items];
|
|
}
|
|
}
|
|
|
|
if (mainOutputs.length === 0) {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
'The node does not have a "Main" output set. Please add one.',
|
|
{
|
|
itemIndex,
|
|
},
|
|
);
|
|
} else if (!options.multiOutput) {
|
|
for (const item of items as INodeExecutionData[]) {
|
|
standardizeOutput(item.json);
|
|
}
|
|
return [items as INodeExecutionData[]];
|
|
} else {
|
|
items.forEach((data) => {
|
|
for (const item of data as INodeExecutionData[]) {
|
|
standardizeOutput(item.json);
|
|
}
|
|
});
|
|
return items as INodeExecutionData[][];
|
|
}
|
|
}
|
|
|
|
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
|
const code = this.getNodeParameter('code', itemIndex) as { supplyData?: { code: string } };
|
|
|
|
if (!code.supplyData?.code) {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
`No code for "Supply Data" set on node "${this.getNode().name}`,
|
|
{
|
|
itemIndex,
|
|
},
|
|
);
|
|
}
|
|
|
|
const sandbox = getSandbox.call(this, code.supplyData.code, { itemIndex });
|
|
const response = await sandbox.runCode<Tool>();
|
|
|
|
return {
|
|
response: logWrapper(response, this),
|
|
};
|
|
}
|
|
}
|