@n8n/n8n-nodes-langchain
Version:

375 lines (371 loc) • 16.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Code = exports.vmResolver = void 0;
exports.transformLegacyLangchainImport = transformLegacyLangchainImport;
exports.createSandboxLogger = createSandboxLogger;
const vm2_1 = require("vm2");
const JavaScriptSandbox_1 = require("n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox");
const Sandbox_1 = require("n8n-nodes-base/dist/nodes/Code/Sandbox");
const utils_1 = require("n8n-nodes-base/dist/nodes/Code/utils");
const n8n_workflow_1 = require("n8n-workflow");
const ai_utilities_1 = require("@n8n/ai-utilities");
const { NODE_FUNCTION_ALLOW_BUILTIN: builtIn, NODE_FUNCTION_ALLOW_EXTERNAL: external } = process.env;
const connectorTypes = {
[n8n_workflow_1.NodeConnectionTypes.AiChain]: 'Chain',
[n8n_workflow_1.NodeConnectionTypes.AiDocument]: 'Document',
[n8n_workflow_1.NodeConnectionTypes.AiEmbedding]: 'Embedding',
[n8n_workflow_1.NodeConnectionTypes.AiLanguageModel]: 'Language Model',
[n8n_workflow_1.NodeConnectionTypes.AiMemory]: 'Memory',
[n8n_workflow_1.NodeConnectionTypes.AiOutputParser]: 'Output Parser',
[n8n_workflow_1.NodeConnectionTypes.AiTextSplitter]: 'Text Splitter',
[n8n_workflow_1.NodeConnectionTypes.AiTool]: 'Tool',
[n8n_workflow_1.NodeConnectionTypes.AiVectorStore]: 'Vector Store',
[n8n_workflow_1.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();`;
function transformLegacyLangchainImport(moduleName) {
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',
];
if (moduleName.startsWith('langchain/')) {
const subpath = moduleName.substring('langchain/'.length);
for (const classicModule of classicModules) {
if (subpath === classicModule || subpath.startsWith(classicModule + '/')) {
return `@langchain/classic/${subpath}`;
}
}
}
return moduleName;
}
function transformLegacyLangchainCode(code) {
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}')`;
});
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) {
return level !== 'silent';
}
function createSandboxLogger(logger) {
return n8n_workflow_1.LOG_LEVELS.filter(isLoggerKey).reduce((result, level) => {
result[level] = (...args) => logger[level].apply(logger, args);
return result;
}, {});
}
const langchainModules = ['langchain', '@langchain/*'];
exports.vmResolver = (0, vm2_1.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(code, options) {
const itemIndex = options?.itemIndex ?? 0;
const node = this.getNode();
const workflowMode = this.getMode();
const transformedCode = transformLegacyLangchainCode(code);
const context = Sandbox_1.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_1.JavaScriptSandbox(context, transformedCode, this.helpers, {
resolver: exports.vmResolver,
});
sandbox.on('output', workflowMode === 'manual'
? this.sendMessageToUI.bind(this)
: (...args) => console.log(`[Workflow "${this.getWorkflow().id}"][Node "${node.name}"]`, ...args));
return sandbox;
}
class Code {
constructor() {
this.description = {
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,
},
],
},
],
},
{
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],
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],
value: key,
})),
noDataExpression: true,
default: '',
required: true,
description: 'The type of the input',
},
],
},
],
},
],
};
}
async execute() {
const itemIndex = 0;
const code = this.getNodeParameter('code', itemIndex);
if (!code.execute?.code) {
throw new n8n_workflow_1.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 = outputs.filter((output) => output.type === n8n_workflow_1.NodeConnectionTypes.Main);
const options = { multiOutput: mainOutputs.length !== 1 };
let items;
try {
items = await sandbox.runCodeAllItems(options);
}
catch (error) {
if (!this.continueOnFail())
throw error;
items = [{ json: { error: error.message } }];
if (options.multiOutput) {
items = [items];
}
}
if (mainOutputs.length === 0) {
throw new n8n_workflow_1.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) {
(0, utils_1.standardizeOutput)(item.json);
}
return [items];
}
else {
items.forEach((data) => {
for (const item of data) {
(0, utils_1.standardizeOutput)(item.json);
}
});
return items;
}
}
async supplyData(itemIndex) {
const code = this.getNodeParameter('code', itemIndex);
if (!code.supplyData?.code) {
throw new n8n_workflow_1.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();
return {
response: (0, ai_utilities_1.logWrapper)(response, this),
};
}
}
exports.Code = Code;
//# sourceMappingURL=Code.node.js.map