diff --git a/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts index 824ea03d35..7d6720adcd 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts @@ -64,7 +64,7 @@ export class OpenAiAssistant implements INodeType { default: 'existing', options: [ { - name: 'Create New Assistant', + name: 'Use New Assistant', value: 'new', }, { @@ -94,7 +94,6 @@ export class OpenAiAssistant implements INodeType { typeOptions: { rows: 5, }, - required: true, displayOptions: { show: { '/mode': ['new'], @@ -237,11 +236,28 @@ export class OpenAiAssistant implements INodeType { value: 'code_interpreter', }, { - name: 'Retrieval', + name: 'Knowledge Retrieval', value: 'retrieval', }, ], }, + { + displayName: 'Connect your own custom tools to this node on the canvas', + name: 'noticeTools', + type: 'notice', + default: '', + }, + { + displayName: + 'Upload files for retrieval using the OpenAI website', + name: 'noticeTools', + type: 'notice', + typeOptions: { + noticeTheme: 'info', + }, + displayOptions: { show: { '/nativeTools': ['retrieval'] } }, + default: '', + }, { displayName: 'Options', name: 'options', diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts index ed6bfb188e..c8e6b887b7 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts @@ -191,13 +191,33 @@ async function getChain( return Array.isArray(response) ? response : [response]; } +function getInputs(parameters: IDataObject) { + const hasOutputParser = parameters?.hasOutputParser; + const inputs = [ + { displayName: '', type: NodeConnectionType.Main }, + { + displayName: 'Model', + maxConnections: 1, + type: NodeConnectionType.AiLanguageModel, + required: true, + }, + ]; + + // If `hasOutputParser` is undefined it must be version 1.1 or earlier so we + // always add the output parser input + if (hasOutputParser === undefined || hasOutputParser === true) { + inputs.push({ displayName: 'Output Parser', type: NodeConnectionType.AiOutputParser }); + } + return inputs; +} + export class ChainLlm implements INodeType { description: INodeTypeDescription = { displayName: 'Basic LLM Chain', name: 'chainLlm', icon: 'fa:link', group: ['transform'], - version: [1, 1.1], + version: [1, 1.1, 1.2], description: 'A simple chain to prompt a large language model', defaults: { name: 'Basic LLM Chain', @@ -217,25 +237,11 @@ export class ChainLlm implements INodeType { ], }, }, - // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node - inputs: [ - NodeConnectionType.Main, - { - displayName: 'Model', - maxConnections: 1, - type: NodeConnectionType.AiLanguageModel, - required: true, - }, - { - displayName: 'Output Parser', - type: NodeConnectionType.AiOutputParser, - required: false, - }, - ], + inputs: `={{ ((parameter) => { ${getInputs.toString()}; return getInputs(parameter) })($parameter) }}`, outputs: [NodeConnectionType.Main], credentials: [], properties: [ - getTemplateNoticeField(1951), + getTemplateNoticeField(1978), { displayName: 'Prompt', name: 'prompt', @@ -256,7 +262,7 @@ export class ChainLlm implements INodeType { default: '={{ $json.chat_input }}', displayOptions: { show: { - '@version': [1.1], + '@version': [1.1, 1.2], }, }, }, @@ -400,6 +406,28 @@ export class ChainLlm implements INodeType { }, ], }, + { + displayName: 'Require Specific Output Format', + name: 'hasOutputParser', + type: 'boolean', + default: false, + displayOptions: { + show: { + '@version': [1.2], + }, + }, + }, + { + displayName: `Connect an output parser on the canvas to specify the output format you require`, + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { + hasOutputParser: [true], + }, + }, + }, ], }; @@ -413,10 +441,14 @@ export class ChainLlm implements INodeType { 0, )) as BaseLanguageModel; - const outputParsers = (await this.getInputConnectionData( - NodeConnectionType.AiOutputParser, - 0, - )) as BaseOutputParser[]; + let outputParsers: BaseOutputParser[] = []; + + if (this.getNodeParameter('hasOutputParser', 0, true) === true) { + outputParsers = (await this.getInputConnectionData( + NodeConnectionType.AiOutputParser, + 0, + )) as BaseOutputParser[]; + } for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const prompt = this.getNodeParameter('prompt', itemIndex) as string; diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts index bed1f047b4..8cc64c6e4f 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts @@ -1,277 +1,39 @@ -import { - NodeConnectionType, - type IExecuteFunctions, - type INodeExecutionData, - type INodeType, - type INodeTypeDescription, -} from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import type { SummarizationChainParams } from 'langchain/chains'; -import { loadSummarizationChain } from 'langchain/chains'; -import type { BaseLanguageModel } from 'langchain/dist/base_language'; -import type { Document } from 'langchain/document'; -import { PromptTemplate } from 'langchain/prompts'; -import { N8nJsonLoader } from '../../../utils/N8nJsonLoader'; -import { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader'; -import { getTemplateNoticeField } from '../../../utils/sharedFields'; -import { REFINE_PROMPT_TEMPLATE, DEFAULT_PROMPT_TEMPLATE } from './prompt'; +import { ChainSummarizationV1 } from './V1/ChainSummarizationV1.node'; +import { ChainSummarizationV2 } from './V2/ChainSummarizationV2.node'; -export class ChainSummarization implements INodeType { - description: INodeTypeDescription = { - displayName: 'Summarization Chain', - name: 'chainSummarization', - icon: 'fa:link', - group: ['transform'], - version: 1, - description: 'Transforms text into a concise summary', - - defaults: { - name: 'Summarization Chain', - color: '#909298', - }, - codex: { - alias: ['LangChain'], - categories: ['AI'], - subcategories: { - AI: ['Chains'], +export class ChainSummarization extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Summarization Chain', + name: 'chainSummarization', + icon: 'fa:link', + group: ['transform'], + description: 'Transforms text into a concise summary', + codex: { + alias: ['LangChain'], + categories: ['AI'], + subcategories: { + AI: ['Chains'], + }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.chainsummarization/', + }, + ], + }, }, - resources: { - primaryDocumentation: [ - { - url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.chainsummarization/', - }, - ], - }, - }, - // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node - inputs: [ - NodeConnectionType.Main, - { - displayName: 'Model', - maxConnections: 1, - type: NodeConnectionType.AiLanguageModel, - required: true, - }, - { - displayName: 'Document', - maxConnections: 1, - type: NodeConnectionType.AiDocument, - required: true, - }, - ], - outputs: [NodeConnectionType.Main], - credentials: [], - properties: [ - getTemplateNoticeField(1951), - { - displayName: 'Type', - name: 'type', - type: 'options', - description: 'The type of summarization to run', - default: 'map_reduce', - options: [ - { - name: 'Map Reduce (Recommended)', - value: 'map_reduce', - description: - 'Summarize each document (or chunk) individually, then summarize those summaries', - }, - { - name: 'Refine', - value: 'refine', - description: - 'Summarize the first document (or chunk). Then update that summary based on the next document (or chunk), and repeat.', - }, - { - name: 'Stuff', - value: 'stuff', - description: 'Pass all documents (or chunks) at once. Ideal for small datasets.', - }, - ], - }, - { - displayName: 'Options', - name: 'options', - type: 'collection', - default: {}, - placeholder: 'Add Option', - options: [ - { - displayName: 'Final Prompt to Combine', - name: 'combineMapPrompt', - type: 'string', - hint: 'The prompt to combine individual summaries', - displayOptions: { - show: { - '/type': ['map_reduce'], - }, - }, - default: DEFAULT_PROMPT_TEMPLATE, - typeOptions: { - rows: 6, - }, - }, - { - displayName: 'Individual Summary Prompt', - name: 'prompt', - type: 'string', - default: DEFAULT_PROMPT_TEMPLATE, - hint: 'The prompt to summarize an individual document (or chunk)', - displayOptions: { - show: { - '/type': ['map_reduce'], - }, - }, - typeOptions: { - rows: 6, - }, - }, - { - displayName: 'Prompt', - name: 'prompt', - type: 'string', - default: DEFAULT_PROMPT_TEMPLATE, - displayOptions: { - show: { - '/type': ['stuff'], - }, - }, - typeOptions: { - rows: 6, - }, - }, - { - displayName: 'Subsequent (Refine) Prompt', - name: 'refinePrompt', - type: 'string', - displayOptions: { - show: { - '/type': ['refine'], - }, - }, - default: REFINE_PROMPT_TEMPLATE, - hint: 'The prompt to refine the summary based on the next document (or chunk)', - typeOptions: { - rows: 6, - }, - }, - { - displayName: 'Initial Prompt', - name: 'refineQuestionPrompt', - type: 'string', - displayOptions: { - show: { - '/type': ['refine'], - }, - }, - default: DEFAULT_PROMPT_TEMPLATE, - hint: 'The prompt for the first document (or chunk)', - typeOptions: { - rows: 6, - }, - }, - ], - }, - ], - }; - - async execute(this: IExecuteFunctions): Promise { - this.logger.verbose('Executing Vector Store QA Chain'); - const type = this.getNodeParameter('type', 0) as 'map_reduce' | 'stuff' | 'refine'; - - const model = (await this.getInputConnectionData( - NodeConnectionType.AiLanguageModel, - 0, - )) as BaseLanguageModel; - - const documentInput = (await this.getInputConnectionData(NodeConnectionType.AiDocument, 0)) as - | N8nJsonLoader - | Array>>; - - const options = this.getNodeParameter('options', 0, {}) as { - prompt?: string; - refineQuestionPrompt?: string; - refinePrompt?: string; - combineMapPrompt?: string; + defaultVersion: 2, }; - const chainArgs: SummarizationChainParams = { - type, + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new ChainSummarizationV1(baseDescription), + 2: new ChainSummarizationV2(baseDescription), }; - // Map reduce prompt override - if (type === 'map_reduce') { - const mapReduceArgs = chainArgs as SummarizationChainParams & { - type: 'map_reduce'; - }; - if (options.combineMapPrompt) { - mapReduceArgs.combineMapPrompt = new PromptTemplate({ - template: options.combineMapPrompt, - inputVariables: ['text'], - }); - } - if (options.prompt) { - mapReduceArgs.combinePrompt = new PromptTemplate({ - template: options.prompt, - inputVariables: ['text'], - }); - } - } - - // Stuff prompt override - if (type === 'stuff') { - const stuffArgs = chainArgs as SummarizationChainParams & { - type: 'stuff'; - }; - if (options.prompt) { - stuffArgs.prompt = new PromptTemplate({ - template: options.prompt, - inputVariables: ['text'], - }); - } - } - - // Refine prompt override - if (type === 'refine') { - const refineArgs = chainArgs as SummarizationChainParams & { - type: 'refine'; - }; - - if (options.refinePrompt) { - refineArgs.refinePrompt = new PromptTemplate({ - template: options.refinePrompt, - inputVariables: ['existing_answer', 'text'], - }); - } - - if (options.refineQuestionPrompt) { - refineArgs.questionPrompt = new PromptTemplate({ - template: options.refineQuestionPrompt, - inputVariables: ['text'], - }); - } - } - - const chain = loadSummarizationChain(model, chainArgs); - - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - let processedDocuments: Document[]; - if (documentInput instanceof N8nJsonLoader || documentInput instanceof N8nBinaryLoader) { - processedDocuments = await documentInput.processItem(items[itemIndex], itemIndex); - } else { - processedDocuments = documentInput; - } - - const response = await chain.call({ - input_documents: processedDocuments, - }); - - returnData.push({ json: { response } }); - } - - return this.prepareOutputData(returnData); + super(nodeVersions, baseDescription); } } diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V1/ChainSummarizationV1.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V1/ChainSummarizationV1.node.ts new file mode 100644 index 0000000000..79347087af --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V1/ChainSummarizationV1.node.ts @@ -0,0 +1,263 @@ +import { + NodeConnectionType, + type INodeTypeBaseDescription, + type IExecuteFunctions, + type INodeExecutionData, + type INodeType, + type INodeTypeDescription, +} from 'n8n-workflow'; + +import type { SummarizationChainParams } from 'langchain/chains'; +import { loadSummarizationChain } from 'langchain/chains'; +import type { BaseLanguageModel } from 'langchain/dist/base_language'; +import type { Document } from 'langchain/document'; +import { PromptTemplate } from 'langchain/prompts'; +import { N8nJsonLoader } from '../../../../utils/N8nJsonLoader'; +import { N8nBinaryLoader } from '../../../../utils/N8nBinaryLoader'; +import { getTemplateNoticeField } from '../../../../utils/sharedFields'; +import { REFINE_PROMPT_TEMPLATE, DEFAULT_PROMPT_TEMPLATE } from '../prompt'; + +export class ChainSummarizationV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + version: 1, + defaults: { + name: 'Summarization Chain', + color: '#909298', + }, + // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node + inputs: [ + NodeConnectionType.Main, + { + displayName: 'Model', + maxConnections: 1, + type: NodeConnectionType.AiLanguageModel, + required: true, + }, + { + displayName: 'Document', + maxConnections: 1, + type: NodeConnectionType.AiDocument, + required: true, + }, + ], + outputs: [NodeConnectionType.Main], + credentials: [], + properties: [ + getTemplateNoticeField(1951), + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of summarization to run', + default: 'map_reduce', + options: [ + { + name: 'Map Reduce (Recommended)', + value: 'map_reduce', + description: + 'Summarize each document (or chunk) individually, then summarize those summaries', + }, + { + name: 'Refine', + value: 'refine', + description: + 'Summarize the first document (or chunk). Then update that summary based on the next document (or chunk), and repeat.', + }, + { + name: 'Stuff', + value: 'stuff', + description: 'Pass all documents (or chunks) at once. Ideal for small datasets.', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Final Prompt to Combine', + name: 'combineMapPrompt', + type: 'string', + hint: 'The prompt to combine individual summaries', + displayOptions: { + show: { + '/type': ['map_reduce'], + }, + }, + default: DEFAULT_PROMPT_TEMPLATE, + typeOptions: { + rows: 6, + }, + }, + { + displayName: 'Individual Summary Prompt', + name: 'prompt', + type: 'string', + default: DEFAULT_PROMPT_TEMPLATE, + hint: 'The prompt to summarize an individual document (or chunk)', + displayOptions: { + show: { + '/type': ['map_reduce'], + }, + }, + typeOptions: { + rows: 6, + }, + }, + { + displayName: 'Prompt', + name: 'prompt', + type: 'string', + default: DEFAULT_PROMPT_TEMPLATE, + displayOptions: { + show: { + '/type': ['stuff'], + }, + }, + typeOptions: { + rows: 6, + }, + }, + { + displayName: 'Subsequent (Refine) Prompt', + name: 'refinePrompt', + type: 'string', + displayOptions: { + show: { + '/type': ['refine'], + }, + }, + default: REFINE_PROMPT_TEMPLATE, + hint: 'The prompt to refine the summary based on the next document (or chunk)', + typeOptions: { + rows: 6, + }, + }, + { + displayName: 'Initial Prompt', + name: 'refineQuestionPrompt', + type: 'string', + displayOptions: { + show: { + '/type': ['refine'], + }, + }, + default: DEFAULT_PROMPT_TEMPLATE, + hint: 'The prompt for the first document (or chunk)', + typeOptions: { + rows: 6, + }, + }, + ], + }, + ], + }; + } + + async execute(this: IExecuteFunctions): Promise { + this.logger.verbose('Executing Vector Store QA Chain'); + const type = this.getNodeParameter('type', 0) as 'map_reduce' | 'stuff' | 'refine'; + + const model = (await this.getInputConnectionData( + NodeConnectionType.AiLanguageModel, + 0, + )) as BaseLanguageModel; + + const documentInput = (await this.getInputConnectionData(NodeConnectionType.AiDocument, 0)) as + | N8nJsonLoader + | Array>>; + + const options = this.getNodeParameter('options', 0, {}) as { + prompt?: string; + refineQuestionPrompt?: string; + refinePrompt?: string; + combineMapPrompt?: string; + }; + + const chainArgs: SummarizationChainParams = { + type, + }; + + // Map reduce prompt override + if (type === 'map_reduce') { + const mapReduceArgs = chainArgs as SummarizationChainParams & { + type: 'map_reduce'; + }; + if (options.combineMapPrompt) { + mapReduceArgs.combineMapPrompt = new PromptTemplate({ + template: options.combineMapPrompt, + inputVariables: ['text'], + }); + } + if (options.prompt) { + mapReduceArgs.combinePrompt = new PromptTemplate({ + template: options.prompt, + inputVariables: ['text'], + }); + } + } + + // Stuff prompt override + if (type === 'stuff') { + const stuffArgs = chainArgs as SummarizationChainParams & { + type: 'stuff'; + }; + if (options.prompt) { + stuffArgs.prompt = new PromptTemplate({ + template: options.prompt, + inputVariables: ['text'], + }); + } + } + + // Refine prompt override + if (type === 'refine') { + const refineArgs = chainArgs as SummarizationChainParams & { + type: 'refine'; + }; + + if (options.refinePrompt) { + refineArgs.refinePrompt = new PromptTemplate({ + template: options.refinePrompt, + inputVariables: ['existing_answer', 'text'], + }); + } + + if (options.refineQuestionPrompt) { + refineArgs.questionPrompt = new PromptTemplate({ + template: options.refineQuestionPrompt, + inputVariables: ['text'], + }); + } + } + + const chain = loadSummarizationChain(model, chainArgs); + + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + let processedDocuments: Document[]; + if (documentInput instanceof N8nJsonLoader || documentInput instanceof N8nBinaryLoader) { + processedDocuments = await documentInput.processItem(items[itemIndex], itemIndex); + } else { + processedDocuments = documentInput; + } + + const response = await chain.call({ + input_documents: processedDocuments, + }); + + returnData.push({ json: { response } }); + } + + return this.prepareOutputData(returnData); + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts new file mode 100644 index 0000000000..784406b156 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/V2/ChainSummarizationV2.node.ts @@ -0,0 +1,420 @@ +import { NodeConnectionType } from 'n8n-workflow'; +import type { + INodeTypeBaseDescription, + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IDataObject, +} from 'n8n-workflow'; + +import { loadSummarizationChain } from 'langchain/chains'; +import type { BaseLanguageModel } from 'langchain/dist/base_language'; +import type { Document } from 'langchain/document'; +import type { TextSplitter } from 'langchain/text_splitter'; +import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'; +import { N8nJsonLoader } from '../../../../utils/N8nJsonLoader'; +import { N8nBinaryLoader } from '../../../../utils/N8nBinaryLoader'; +import { getTemplateNoticeField } from '../../../../utils/sharedFields'; +import { REFINE_PROMPT_TEMPLATE, DEFAULT_PROMPT_TEMPLATE } from '../prompt'; +import { getChainPromptsArgs } from '../helpers'; + +function getInputs(parameters: IDataObject) { + const chunkingMode = parameters?.chunkingMode; + const operationMode = parameters?.operationMode; + const inputs = [ + { displayName: '', type: NodeConnectionType.Main }, + { + displayName: 'Model', + maxConnections: 1, + type: NodeConnectionType.AiLanguageModel, + required: true, + }, + ]; + + if (operationMode === 'documentLoader') { + inputs.push({ + displayName: 'Document', + type: NodeConnectionType.AiDocument, + required: true, + maxConnections: 1, + }); + return inputs; + } + + if (chunkingMode === 'advanced') { + inputs.push({ + displayName: 'Text Splitter', + type: NodeConnectionType.AiTextSplitter, + required: false, + maxConnections: 1, + }); + return inputs; + } + return inputs; +} + +export class ChainSummarizationV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + version: [2], + defaults: { + name: 'Summarization Chain', + color: '#909298', + }, + // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node + inputs: `={{ ((parameter) => { ${getInputs.toString()}; return getInputs(parameter) })($parameter) }}`, + outputs: [NodeConnectionType.Main], + credentials: [], + properties: [ + getTemplateNoticeField(1951), + { + displayName: 'Data to Summarize', + name: 'operationMode', + noDataExpression: true, + type: 'options', + description: 'How to pass data into the summarization chain', + default: 'nodeInputJson', + options: [ + { + name: 'Use Node Input (JSON)', + value: 'nodeInputJson', + description: 'Summarize the JSON data coming into this node from the previous one', + }, + { + name: 'Use Node Input (Binary)', + value: 'nodeInputBinary', + description: 'Summarize the binary data coming into this node from the previous one', + }, + { + name: 'Use Document Loader', + value: 'documentLoader', + description: 'Use a loader sub-node with more configuration options', + }, + ], + }, + { + displayName: 'Chunking Strategy', + name: 'chunkingMode', + noDataExpression: true, + type: 'options', + description: 'Chunk splitting strategy', + default: 'simple', + options: [ + { + name: 'Simple (Define Below)', + value: 'simple', + }, + { + name: 'Advanced', + value: 'advanced', + description: 'Use a splitter sub-node with more configuration options', + }, + ], + displayOptions: { + show: { + '/operationMode': ['nodeInputJson', 'nodeInputBinary'], + }, + }, + }, + { + displayName: 'Characters Per Chunk', + name: 'chunkSize', + description: + 'Controls the max size (in terms of number of characters) of the final document chunk', + type: 'number', + default: 1000, + displayOptions: { + show: { + '/chunkingMode': ['simple'], + }, + }, + }, + { + displayName: 'Chunk Overlap (Characters)', + name: 'chunkOverlap', + type: 'number', + description: 'Specifies how much characters overlap there should be between chunks', + default: 200, + displayOptions: { + show: { + '/chunkingMode': ['simple'], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Input Data Field Name', + name: 'binaryDataKey', + type: 'string', + default: 'data', + description: + 'The name of the field in the agent or chain’s input that contains the binary file to be processed', + displayOptions: { + show: { + '/operationMode': ['nodeInputBinary'], + }, + }, + }, + { + displayName: 'Summarization Method and Prompts', + name: 'summarizationMethodAndPrompts', + type: 'fixedCollection', + default: { + values: { + summarizationMethod: 'map_reduce', + prompt: DEFAULT_PROMPT_TEMPLATE, + combineMapPrompt: DEFAULT_PROMPT_TEMPLATE, + }, + }, + placeholder: 'Add Option', + typeOptions: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Summarization Method', + name: 'summarizationMethod', + type: 'options', + description: 'The type of summarization to run', + default: 'map_reduce', + options: [ + { + name: 'Map Reduce (Recommended)', + value: 'map_reduce', + description: + 'Summarize each document (or chunk) individually, then summarize those summaries', + }, + { + name: 'Refine', + value: 'refine', + description: + 'Summarize the first document (or chunk). Then update that summary based on the next document (or chunk), and repeat.', + }, + { + name: 'Stuff', + value: 'stuff', + description: + 'Pass all documents (or chunks) at once. Ideal for small datasets.', + }, + ], + }, + { + displayName: 'Final Prompt to Combine', + name: 'combineMapPrompt', + type: 'string', + hint: 'The prompt to combine individual summaries', + displayOptions: { + hide: { + '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ + 'stuff', + 'refine', + ], + }, + }, + default: DEFAULT_PROMPT_TEMPLATE, + typeOptions: { + rows: 9, + }, + }, + { + displayName: 'Individual Summary Prompt', + name: 'prompt', + type: 'string', + default: DEFAULT_PROMPT_TEMPLATE, + hint: 'The prompt to summarize an individual document (or chunk)', + displayOptions: { + hide: { + '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ + 'stuff', + 'refine', + ], + }, + }, + typeOptions: { + rows: 9, + }, + }, + { + displayName: 'Prompt', + name: 'prompt', + type: 'string', + default: DEFAULT_PROMPT_TEMPLATE, + displayOptions: { + hide: { + '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ + 'refine', + 'map_reduce', + ], + }, + }, + typeOptions: { + rows: 9, + }, + }, + { + displayName: 'Subsequent (Refine) Prompt', + name: 'refinePrompt', + type: 'string', + displayOptions: { + hide: { + '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ + 'stuff', + 'map_reduce', + ], + }, + }, + default: REFINE_PROMPT_TEMPLATE, + hint: 'The prompt to refine the summary based on the next document (or chunk)', + typeOptions: { + rows: 9, + }, + }, + { + displayName: 'Initial Prompt', + name: 'refineQuestionPrompt', + type: 'string', + displayOptions: { + hide: { + '/options.summarizationMethodAndPrompts.values.summarizationMethod': [ + 'stuff', + 'map_reduce', + ], + }, + }, + default: DEFAULT_PROMPT_TEMPLATE, + hint: 'The prompt for the first document (or chunk)', + typeOptions: { + rows: 9, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }; + } + + async execute(this: IExecuteFunctions): Promise { + this.logger.verbose('Executing Summarization Chain V2'); + const operationMode = this.getNodeParameter('operationMode', 0, 'nodeInputJson') as + | 'nodeInputJson' + | 'nodeInputBinary' + | 'documentLoader'; + const chunkingMode = this.getNodeParameter('chunkingMode', 0, 'simple') as + | 'simple' + | 'advanced'; + + const model = (await this.getInputConnectionData( + NodeConnectionType.AiLanguageModel, + 0, + )) as BaseLanguageModel; + + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const summarizationMethodAndPrompts = this.getNodeParameter( + 'options.summarizationMethodAndPrompts.values', + itemIndex, + {}, + ) as { + prompt?: string; + refineQuestionPrompt?: string; + refinePrompt?: string; + summarizationMethod: 'map_reduce' | 'stuff' | 'refine'; + combineMapPrompt?: string; + }; + + const chainArgs = getChainPromptsArgs( + summarizationMethodAndPrompts.summarizationMethod ?? 'map_reduce', + summarizationMethodAndPrompts, + ); + + const chain = loadSummarizationChain(model, chainArgs); + const item = items[itemIndex]; + + let processedDocuments: Document[]; + + // Use dedicated document loader input to load documents + if (operationMode === 'documentLoader') { + const documentInput = (await this.getInputConnectionData( + NodeConnectionType.AiDocument, + 0, + )) as N8nJsonLoader | Array>>; + + const isN8nLoader = + documentInput instanceof N8nJsonLoader || documentInput instanceof N8nBinaryLoader; + + processedDocuments = isN8nLoader + ? await documentInput.processItem(item, itemIndex) + : documentInput; + + const response = await chain.call({ + input_documents: processedDocuments, + }); + + returnData.push({ json: { response } }); + } + + // Take the input and use binary or json loader + if (['nodeInputJson', 'nodeInputBinary'].includes(operationMode)) { + let textSplitter: TextSplitter | undefined; + + switch (chunkingMode) { + // In simple mode we use recursive character splitter with default settings + case 'simple': + const chunkSize = this.getNodeParameter('chunkSize', itemIndex, 1000) as number; + const chunkOverlap = this.getNodeParameter('chunkOverlap', itemIndex, 200) as number; + + textSplitter = new RecursiveCharacterTextSplitter({ chunkOverlap, chunkSize }); + break; + + // In advanced mode user can connect text splitter node so we just retrieve it + case 'advanced': + textSplitter = (await this.getInputConnectionData( + NodeConnectionType.AiTextSplitter, + 0, + )) as TextSplitter | undefined; + break; + default: + break; + } + + let processor: N8nJsonLoader | N8nBinaryLoader; + if (operationMode === 'nodeInputBinary') { + const binaryDataKey = this.getNodeParameter( + 'options.binaryDataKey', + itemIndex, + 'data', + ) as string; + processor = new N8nBinaryLoader(this, 'options.', binaryDataKey, textSplitter); + } else { + processor = new N8nJsonLoader(this, 'options.', textSplitter); + } + + const processedItem = await processor.processItem(item, itemIndex); + const response = await chain.call({ + input_documents: processedItem, + }); + returnData.push({ json: { response } }); + } + } + + return this.prepareOutputData(returnData); + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/helpers.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/helpers.ts new file mode 100644 index 0000000000..e24e21b77f --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/helpers.ts @@ -0,0 +1,72 @@ +import type { SummarizationChainParams } from 'langchain/chains'; +import { PromptTemplate } from 'langchain/prompts'; + +interface ChainTypeOptions { + combineMapPrompt?: string; + prompt?: string; + refinePrompt?: string; + refineQuestionPrompt?: string; +} + +export function getChainPromptsArgs( + type: 'stuff' | 'map_reduce' | 'refine', + options: ChainTypeOptions, +) { + const chainArgs: SummarizationChainParams = { + type, + }; + // Map reduce prompt override + if (type === 'map_reduce') { + const mapReduceArgs = chainArgs as SummarizationChainParams & { + type: 'map_reduce'; + }; + if (options.combineMapPrompt) { + mapReduceArgs.combineMapPrompt = new PromptTemplate({ + template: options.combineMapPrompt, + inputVariables: ['text'], + }); + } + if (options.prompt) { + mapReduceArgs.combinePrompt = new PromptTemplate({ + template: options.prompt, + inputVariables: ['text'], + }); + } + } + + // Stuff prompt override + if (type === 'stuff') { + const stuffArgs = chainArgs as SummarizationChainParams & { + type: 'stuff'; + }; + if (options.prompt) { + stuffArgs.prompt = new PromptTemplate({ + template: options.prompt, + inputVariables: ['text'], + }); + } + } + + // Refine prompt override + if (type === 'refine') { + const refineArgs = chainArgs as SummarizationChainParams & { + type: 'refine'; + }; + + if (options.refinePrompt) { + refineArgs.refinePrompt = new PromptTemplate({ + template: options.refinePrompt, + inputVariables: ['existing_answer', 'text'], + }); + } + + if (options.refineQuestionPrompt) { + refineArgs.questionPrompt = new PromptTemplate({ + template: options.refineQuestionPrompt, + inputVariables: ['text'], + }); + } + } + + return chainArgs; +} diff --git a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts index 1b9e1fbe6f..b5c68cde63 100644 --- a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentBinaryInputLoader/DocumentBinaryInputLoader.node.ts @@ -17,6 +17,7 @@ import { getConnectionHintNoticeField, metadataFilterField } from '../../../util import 'mammoth'; // for docx import 'epub2'; // for epub import 'pdf-parse'; // for pdf +import type { TextSplitter } from 'langchain/text_splitter'; export class DocumentBinaryInputLoader implements INodeType { description: INodeTypeDescription = { @@ -177,7 +178,13 @@ export class DocumentBinaryInputLoader implements INodeType { async supplyData(this: IExecuteFunctions): Promise { this.logger.verbose('Supply Data for Binary Input Loader'); - const processor = new N8nBinaryLoader(this); + const textSplitter = (await this.getInputConnectionData( + NodeConnectionType.AiTextSplitter, + 0, + )) as TextSplitter | undefined; + + const binaryDataKey = this.getNodeParameter('binaryDataKey', 0) as string; + const processor = new N8nBinaryLoader(this, undefined, binaryDataKey, textSplitter); return { response: logWrapper(processor, this), diff --git a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentDefaultDataLoader/DocumentDefaultDataLoader.node.ts b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentDefaultDataLoader/DocumentDefaultDataLoader.node.ts index abc6fb4a1c..cb9d66ac9d 100644 --- a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentDefaultDataLoader/DocumentDefaultDataLoader.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentDefaultDataLoader/DocumentDefaultDataLoader.node.ts @@ -7,6 +7,7 @@ import { type SupplyData, } from 'n8n-workflow'; +import type { TextSplitter } from 'langchain/text_splitter'; import { logWrapper } from '../../../utils/logWrapper'; import { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader'; import { metadataFilterField } from '../../../utils/sharedFields'; @@ -257,11 +258,16 @@ export class DocumentDefaultDataLoader implements INodeType { async supplyData(this: IExecuteFunctions, itemIndex: number): Promise { const dataType = this.getNodeParameter('dataType', itemIndex, 'json') as 'json' | 'binary'; + const textSplitter = (await this.getInputConnectionData( + NodeConnectionType.AiTextSplitter, + 0, + )) as TextSplitter | undefined; + const binaryDataKey = this.getNodeParameter('binaryDataKey', itemIndex, '') as string; const processor = dataType === 'binary' - ? new N8nBinaryLoader(this, 'options.') - : new N8nJsonLoader(this, 'options.'); + ? new N8nBinaryLoader(this, 'options.', binaryDataKey, textSplitter) + : new N8nJsonLoader(this, 'options.', textSplitter); return { response: logWrapper(processor, this), diff --git a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentJSONInputLoader/DocumentJsonInputLoader.node.ts b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentJSONInputLoader/DocumentJsonInputLoader.node.ts index bb3af2e7e9..da650d26b4 100644 --- a/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentJSONInputLoader/DocumentJsonInputLoader.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/document_loaders/DocumentJSONInputLoader/DocumentJsonInputLoader.node.ts @@ -7,6 +7,7 @@ import { type SupplyData, } from 'n8n-workflow'; +import type { TextSplitter } from 'langchain/text_splitter'; import { logWrapper } from '../../../utils/logWrapper'; import { N8nJsonLoader } from '../../../utils/N8nJsonLoader'; import { getConnectionHintNoticeField, metadataFilterField } from '../../../utils/sharedFields'; @@ -80,7 +81,12 @@ export class DocumentJsonInputLoader implements INodeType { async supplyData(this: IExecuteFunctions): Promise { this.logger.verbose('Supply Data for JSON Input Loader'); - const processor = new N8nJsonLoader(this); + const textSplitter = (await this.getInputConnectionData( + NodeConnectionType.AiTextSplitter, + 0, + )) as TextSplitter | undefined; + + const processor = new N8nJsonLoader(this, undefined, textSplitter); return { response: logWrapper(processor, this), diff --git a/packages/@n8n/nodes-langchain/utils/N8nBinaryLoader.ts b/packages/@n8n/nodes-langchain/utils/N8nBinaryLoader.ts index 51067e790c..1132a656de 100644 --- a/packages/@n8n/nodes-langchain/utils/N8nBinaryLoader.ts +++ b/packages/@n8n/nodes-langchain/utils/N8nBinaryLoader.ts @@ -1,5 +1,5 @@ import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; -import { NodeOperationError, NodeConnectionType, BINARY_ENCODING } from 'n8n-workflow'; +import { NodeOperationError, BINARY_ENCODING } from 'n8n-workflow'; import type { TextSplitter } from 'langchain/text_splitter'; import type { Document } from 'langchain/document'; @@ -30,9 +30,15 @@ export class N8nBinaryLoader { private optionsPrefix: string; - constructor(context: IExecuteFunctions, optionsPrefix = '') { + private binaryDataKey: string; + + private textSplitter?: TextSplitter; + + constructor(context: IExecuteFunctions, optionsPrefix = '', binaryDataKey = '', textSplitter?: TextSplitter) { this.context = context; + this.textSplitter = textSplitter; this.optionsPrefix = optionsPrefix; + this.binaryDataKey = binaryDataKey; } async processAll(items?: INodeExecutionData[]): Promise { @@ -53,17 +59,15 @@ export class N8nBinaryLoader { const selectedLoader: keyof typeof SUPPORTED_MIME_TYPES = this.context.getNodeParameter( 'loader', itemIndex, + 'auto', ) as keyof typeof SUPPORTED_MIME_TYPES; - const binaryDataKey = this.context.getNodeParameter('binaryDataKey', itemIndex) as string; const docs: Document[] = []; const metadata = getMetadataFiltersValues(this.context, itemIndex); if (!item) return []; - // TODO: Should we support traversing the object to find the binary data? - const binaryData = this.context.helpers.assertBinaryData(itemIndex, binaryDataKey); - + const binaryData = this.context.helpers.assertBinaryData(itemIndex, this.binaryDataKey) const { mimeType } = binaryData; // Check if loader matches the mime-type of the data @@ -174,12 +178,8 @@ export class N8nBinaryLoader { loader = new TextLoader(filePathOrBlob); } - const textSplitter = (await this.context.getInputConnectionData( - NodeConnectionType.AiTextSplitter, - 0, - )) as TextSplitter | undefined; - const loadedDoc = textSplitter ? await loader.loadAndSplit(textSplitter) : await loader.load(); + const loadedDoc = this.textSplitter ? await loader.loadAndSplit(this.textSplitter) : await loader.load(); docs.push(...loadedDoc); diff --git a/packages/@n8n/nodes-langchain/utils/N8nJsonLoader.ts b/packages/@n8n/nodes-langchain/utils/N8nJsonLoader.ts index 882a83ce0a..bf3fb2926c 100644 --- a/packages/@n8n/nodes-langchain/utils/N8nJsonLoader.ts +++ b/packages/@n8n/nodes-langchain/utils/N8nJsonLoader.ts @@ -1,11 +1,10 @@ import { type IExecuteFunctions, type INodeExecutionData, - NodeConnectionType, NodeOperationError, } from 'n8n-workflow'; -import type { CharacterTextSplitter } from 'langchain/text_splitter'; +import type { TextSplitter } from 'langchain/text_splitter'; import type { Document } from 'langchain/document'; import { JSONLoader } from 'langchain/document_loaders/fs/json'; import { TextLoader } from 'langchain/document_loaders/fs/text'; @@ -16,8 +15,11 @@ export class N8nJsonLoader { private optionsPrefix: string; - constructor(context: IExecuteFunctions, optionsPrefix = '') { + private textSplitter?: TextSplitter; + + constructor(context: IExecuteFunctions, optionsPrefix = '', textSplitter?: TextSplitter) { this.context = context; + this.textSplitter = textSplitter; this.optionsPrefix = optionsPrefix; } @@ -46,11 +48,6 @@ export class N8nJsonLoader { '', ) as string; const pointersArray = pointers.split(',').map((pointer) => pointer.trim()); - - const textSplitter = (await this.context.getInputConnectionData( - NodeConnectionType.AiTextSplitter, - 0, - )) as CharacterTextSplitter | undefined; const metadata = getMetadataFiltersValues(this.context, itemIndex) ?? []; if (!item) return []; @@ -81,8 +78,8 @@ export class N8nJsonLoader { throw new NodeOperationError(this.context.getNode(), 'Document loader is not initialized'); } - const docs = textSplitter - ? await documentLoader.loadAndSplit(textSplitter) + const docs = this.textSplitter + ? await documentLoader.loadAndSplit(this.textSplitter) : await documentLoader.load(); if (metadata) { diff --git a/packages/@n8n/nodes-langchain/utils/logWrapper.ts b/packages/@n8n/nodes-langchain/utils/logWrapper.ts index ac68fe6213..52a24ef6ac 100644 --- a/packages/@n8n/nodes-langchain/utils/logWrapper.ts +++ b/packages/@n8n/nodes-langchain/utils/logWrapper.ts @@ -48,6 +48,8 @@ export async function callMethodAsync( try { return await parameters.method.call(this, ...parameters.arguments); } catch (e) { + // Propagate errors from sub-nodes + if (e.functionality === 'configuration-node') throw e; const connectedNode = parameters.executeFunctions.getNode(); const error = new NodeOperationError(connectedNode, e, { @@ -89,6 +91,8 @@ export function callMethodSync( try { return parameters.method.call(this, ...parameters.arguments); } catch (e) { + // Propagate errors from sub-nodes + if (e.functionality === 'configuration-node') throw e; const connectedNode = parameters.executeFunctions.getNode(); const error = new NodeOperationError(connectedNode, e); parameters.executeFunctions.addOutputData( diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 079acb567a..6ff2bbd1bd 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -3282,6 +3282,8 @@ export function getExecuteFunctions( try { return await nodeType.supplyData.call(context, itemIndex); } catch (error) { + // Propagate errors from sub-nodes + if (error.functionality === 'configuration-node') throw error; if (!(error instanceof ExecutionBaseError)) { error = new NodeOperationError(connectedNode, error, { itemIndex, diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue index 38dced8c0c..af445b6996 100644 --- a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue +++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue @@ -2,12 +2,14 @@ import { useI18n } from '@/composables/useI18n'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import N8nTooltip from '../N8nTooltip'; +import { ElTag } from 'element-plus'; export interface Props { active?: boolean; isAi?: boolean; isTrigger?: boolean; description?: string; + tag?: string; title: string; showActionArrow?: boolean; } @@ -35,6 +37,9 @@ const i18n = useI18n();
+ + {{ tag }} + { const nodeCreatorStore = useNodeCreatorStore(); const instance = getCurrentInstance(); + const singleNodeOpenSources = [ + NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, + NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION, + NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP, + ]; + const actionsCategoryLocales = computed(() => { return { actions: instance?.proxy.$locale.baseText('nodeCreator.actionsCategory.actions') ?? '', @@ -156,11 +167,6 @@ export const useActions = () => { const workflowContainsTrigger = workflowTriggerNodes.length > 0; const isTriggerPanel = selectedView === TRIGGER_NODE_CREATOR_VIEW; const onlyStickyNodes = addedNodes.every((node) => node.type === STICKY_NODE_TYPE); - const singleNodeOpenSources = [ - NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, - NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION, - NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP, - ]; // If the node creator was opened from the plus endpoint, node connection action, or node connection drop // then we do not want to append the manual trigger @@ -173,6 +179,22 @@ export const useActions = () => { !onlyStickyNodes ); } + function shouldPrependChatTrigger(addedNodes: AddedNode[]): boolean { + const { allNodes } = useWorkflowsStore(); + + const COMPATIBLE_CHAT_NODES = [ + QA_CHAIN_NODE_TYPE, + AGENT_NODE_TYPE, + BASIC_CHAIN_NODE_TYPE, + OPEN_AI_ASSISTANT_NODE_TYPE, + ]; + + const isChatTriggerMissing = + allNodes.find((node) => node.type === MANUAL_CHAT_TRIGGER_NODE_TYPE) === undefined; + const isCompatibleNode = addedNodes.some((node) => COMPATIBLE_CHAT_NODES.includes(node.type)); + + return isCompatibleNode && isChatTriggerMissing; + } function getAddedNodesAndConnections(addedNodes: AddedNode[]): AddedNodesAndConnections { if (addedNodes.length === 0) { @@ -188,7 +210,13 @@ export const useActions = () => { nodeToAutoOpen.openDetail = true; } - if (shouldPrependManualTrigger(addedNodes)) { + if (shouldPrependChatTrigger(addedNodes)) { + addedNodes.unshift({ type: MANUAL_CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true }); + connections.push({ + from: { nodeIndex: 0 }, + to: { nodeIndex: 1 }, + }); + } else if (shouldPrependManualTrigger(addedNodes)) { addedNodes.unshift({ type: MANUAL_TRIGGER_NODE_TYPE, isAutoAdd: true }); connections.push({ from: { nodeIndex: 0 }, diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index fa674889f8..91a8088807 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -76,6 +76,7 @@ export interface NodeViewItem { group?: string[]; sections?: NodeViewItemSection[]; description?: string; + tag?: string; forceIncludeNodes?: string[]; }; category?: string | string[]; @@ -451,6 +452,7 @@ export function RegularView(nodes: SimplifiedNodeType[]) { title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'), icon: 'robot', description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'), + tag: i18n.baseText('nodeCreator.aiPanel.newTag'), }, }); diff --git a/packages/editor-ui/src/components/RunDataAi/useAiContentParsers.ts b/packages/editor-ui/src/components/RunDataAi/useAiContentParsers.ts index ddcd8b9a4c..723e53b040 100644 --- a/packages/editor-ui/src/components/RunDataAi/useAiContentParsers.ts +++ b/packages/editor-ui/src/components/RunDataAi/useAiContentParsers.ts @@ -88,7 +88,11 @@ const outputTypeParsers: { if (Array.isArray(chatHistory)) { const responseText = chatHistory .map((content: MemoryMessage) => { - if (content.type === 'constructor' && content.id?.includes('schema') && content.kwargs) { + if ( + content.type === 'constructor' && + content.id?.includes('messages') && + content.kwargs + ) { interface MessageContent { type: string; image_url?: { diff --git a/packages/editor-ui/src/components/WorkflowLMChat.vue b/packages/editor-ui/src/components/WorkflowLMChat.vue index b6a71a66b8..7f5f269f38 100644 --- a/packages/editor-ui/src/components/WorkflowLMChat.vue +++ b/packages/editor-ui/src/components/WorkflowLMChat.vue @@ -16,11 +16,12 @@ >