mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(Structured Output Parser Node): Refactor Output Parsers and Improve Error Handling (#11148)
This commit is contained in:
@@ -1,51 +0,0 @@
|
||||
import { BaseOutputParser, OutputParserException } from '@langchain/core/output_parsers';
|
||||
|
||||
export class ItemListOutputParser extends BaseOutputParser<string[]> {
|
||||
lc_namespace = ['n8n-nodes-langchain', 'output_parsers', 'list_items'];
|
||||
|
||||
private numberOfItems: number | undefined;
|
||||
|
||||
private separator: string;
|
||||
|
||||
constructor(options: { numberOfItems?: number; separator?: string }) {
|
||||
super();
|
||||
if (options.numberOfItems && options.numberOfItems > 0) {
|
||||
this.numberOfItems = options.numberOfItems;
|
||||
}
|
||||
this.separator = options.separator ?? '\\n';
|
||||
if (this.separator === '\\n') {
|
||||
this.separator = '\n';
|
||||
}
|
||||
}
|
||||
|
||||
async parse(text: string): Promise<string[]> {
|
||||
const response = text
|
||||
.split(this.separator)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item);
|
||||
|
||||
if (this.numberOfItems && response.length < this.numberOfItems) {
|
||||
// Only error if to few items got returned, if there are to many we can autofix it
|
||||
throw new OutputParserException(
|
||||
`Wrong number of items returned. Expected ${this.numberOfItems} items but got ${response.length} items instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.slice(0, this.numberOfItems);
|
||||
}
|
||||
|
||||
getFormatInstructions(): string {
|
||||
const instructions = `Your response should be a list of ${
|
||||
this.numberOfItems ? this.numberOfItems + ' ' : ''
|
||||
}items separated by`;
|
||||
|
||||
const numberOfExamples = this.numberOfItems ?? 3;
|
||||
|
||||
const examples: string[] = [];
|
||||
for (let i = 1; i <= numberOfExamples; i++) {
|
||||
examples.push(`item${i}`);
|
||||
}
|
||||
|
||||
return `${instructions} "${this.separator}" (for example: "${examples.join(this.separator)}")`;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
type INodeTypeDescription,
|
||||
type SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
import { logWrapper } from '../../../utils/logWrapper';
|
||||
|
||||
import { N8nItemListOutputParser } from '../../../utils/output_parsers/N8nItemListOutputParser';
|
||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||
import { ItemListOutputParser } from './ItemListOutputParser';
|
||||
|
||||
export class OutputParserItemList implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -86,10 +86,10 @@ export class OutputParserItemList implements INodeType {
|
||||
separator?: string;
|
||||
};
|
||||
|
||||
const parser = new ItemListOutputParser(options);
|
||||
const parser = new N8nItemListOutputParser(options);
|
||||
|
||||
return {
|
||||
response: logWrapper(parser, this),
|
||||
response: parser,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { normalizeItems } from 'n8n-core';
|
||||
import {
|
||||
ApplicationError,
|
||||
type IExecuteFunctions,
|
||||
type IWorkflowDataProxyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { N8nItemListOutputParser } from '../../../../utils/output_parsers/N8nItemListOutputParser';
|
||||
import { OutputParserItemList } from '../OutputParserItemList.node';
|
||||
|
||||
describe('OutputParserItemList', () => {
|
||||
let outputParser: OutputParserItemList;
|
||||
const thisArg = mock<IExecuteFunctions>({
|
||||
helpers: { normalizeItems },
|
||||
});
|
||||
const workflowDataProxy = mock<IWorkflowDataProxyData>({ $input: mock() });
|
||||
|
||||
beforeEach(() => {
|
||||
outputParser = new OutputParserItemList();
|
||||
thisArg.getWorkflowDataProxy.mockReturnValue(workflowDataProxy);
|
||||
thisArg.addInputData.mockReturnValue({ index: 0 });
|
||||
thisArg.addOutputData.mockReturnValue();
|
||||
thisArg.getNodeParameter.mockReset();
|
||||
});
|
||||
|
||||
describe('supplyData', () => {
|
||||
it('should create a parser with default options', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return {};
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
expect(response).toBeInstanceOf(N8nItemListOutputParser);
|
||||
});
|
||||
|
||||
it('should create a parser with custom number of items', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return { numberOfItems: 5 };
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
expect(response).toBeInstanceOf(N8nItemListOutputParser);
|
||||
expect((response as any).numberOfItems).toBe(5);
|
||||
});
|
||||
|
||||
it('should create a parser with custom separator', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return { separator: ',' };
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
expect(response).toBeInstanceOf(N8nItemListOutputParser);
|
||||
expect((response as any).separator).toBe(',');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('should parse a list with default separator', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return {};
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
const result = await (response as N8nItemListOutputParser).parse('item1\nitem2\nitem3');
|
||||
expect(result).toEqual(['item1', 'item2', 'item3']);
|
||||
});
|
||||
|
||||
it('should parse a list with custom separator', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return { separator: ',' };
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
const result = await (response as N8nItemListOutputParser).parse('item1,item2,item3');
|
||||
expect(result).toEqual(['item1', 'item2', 'item3']);
|
||||
});
|
||||
|
||||
it('should limit the number of items returned', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return { numberOfItems: 2 };
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
const result = await (response as N8nItemListOutputParser).parse(
|
||||
'item1\nitem2\nitem3\nitem4',
|
||||
);
|
||||
expect(result).toEqual(['item1', 'item2']);
|
||||
});
|
||||
|
||||
it('should throw an error if not enough items are returned', async () => {
|
||||
thisArg.getNodeParameter.mockImplementation((parameterName) => {
|
||||
if (parameterName === 'options') {
|
||||
return { numberOfItems: 5 };
|
||||
}
|
||||
throw new ApplicationError('Not implemented');
|
||||
});
|
||||
|
||||
const { response } = await outputParser.supplyData.call(thisArg, 0);
|
||||
await expect(
|
||||
(response as N8nItemListOutputParser).parse('item1\nitem2\nitem3'),
|
||||
).rejects.toThrow('Wrong number of items returned');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user