first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,334 @@
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import type { JSONSchema7 } from 'json-schema';
import { OutputFixingParser, StructuredOutputParser } from '@langchain/classic/output_parsers';
import { jsonParse, NodeConnectionTypes, NodeOperationError, sleep } from 'n8n-workflow';
import type {
INodeType,
INodeTypeDescription,
IExecuteFunctions,
INodeExecutionData,
INodePropertyOptions,
} from 'n8n-workflow';
import type { z } from 'zod';
import {
buildJsonSchemaExampleNotice,
inputSchemaField,
jsonSchemaExampleField,
schemaTypeField,
} from '@utils/descriptions';
import { convertJsonSchemaToZod, generateSchemaFromExample } from '@utils/schemaParsing';
import { getBatchingOptionFields } from '@n8n/ai-utilities';
import { SYSTEM_PROMPT_TEMPLATE } from './constants';
import { makeZodSchemaFromAttributes } from './helpers';
import { processItem } from './processItem';
import type { AttributeDefinition } from './types';
export class InformationExtractor implements INodeType {
description: INodeTypeDescription = {
displayName: 'Information Extractor',
name: 'informationExtractor',
icon: 'fa:project-diagram',
iconColor: 'black',
group: ['transform'],
version: [1, 1.1, 1.2],
defaultVersion: 1.2,
description: 'Extract information from text in a structured format',
codex: {
alias: ['NER', 'parse', 'parsing', 'JSON', 'data extraction', 'structured'],
categories: ['AI'],
subcategories: {
AI: ['Chains', 'Root Nodes'],
},
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.information-extractor/',
},
],
},
},
defaults: {
name: 'Information Extractor',
},
inputs: [
{ displayName: '', type: NodeConnectionTypes.Main },
{
displayName: 'Model',
maxConnections: 1,
type: NodeConnectionTypes.AiLanguageModel,
required: true,
},
],
outputs: [NodeConnectionTypes.Main],
builderHint: {
inputs: {
ai_languageModel: { required: true },
},
},
properties: [
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
description: 'The text to extract information from',
typeOptions: {
rows: 2,
},
},
{
...schemaTypeField,
description: 'How to specify the schema for the desired output',
options: [
{
name: 'From Attribute Descriptions',
value: 'fromAttributes',
description:
'Extract specific attributes from the text based on types and descriptions',
} as INodePropertyOptions,
...(schemaTypeField.options as INodePropertyOptions[]),
],
default: 'fromAttributes',
},
{
...jsonSchemaExampleField,
default: `{
"state": "California",
"cities": ["Los Angeles", "San Francisco", "San Diego"]
}`,
},
buildJsonSchemaExampleNotice({
showExtraProps: {
'@version': [{ _cnd: { gte: 1.2 } }],
},
}),
{
...inputSchemaField,
default: `{
"type": "object",
"properties": {
"state": {
"type": "string"
},
"cities": {
"type": "array",
"items": {
"type": "string"
}
}
}
}`,
},
{
displayName: 'Attributes',
name: 'attributes',
placeholder: 'Add Attribute',
type: 'fixedCollection',
default: {},
displayOptions: {
show: {
schemaType: ['fromAttributes'],
},
},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attributes',
displayName: 'Attribute List',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Attribute to extract',
placeholder: 'e.g. company_name',
required: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
description: 'Data type of the attribute',
required: true,
options: [
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Date',
value: 'date',
},
{
name: 'Number',
value: 'number',
},
{
name: 'String',
value: 'string',
},
],
default: 'string',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'Describe your attribute',
placeholder: 'Add description for the attribute',
required: true,
},
{
displayName: 'Required',
name: 'required',
type: 'boolean',
default: false,
description: 'Whether attribute is required',
required: true,
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Option',
options: [
{
displayName: 'System Prompt Template',
name: 'systemPromptTemplate',
type: 'string',
default: SYSTEM_PROMPT_TEMPLATE,
description: 'String to use directly as the system prompt template',
typeOptions: {
rows: 6,
},
},
getBatchingOptionFields({
show: {
'@version': [{ _cnd: { gte: 1.1 } }],
},
}),
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const llm = (await this.getInputConnectionData(
NodeConnectionTypes.AiLanguageModel,
0,
)) as BaseLanguageModel;
const schemaType = this.getNodeParameter('schemaType', 0, '') as
| 'fromAttributes'
| 'fromJson'
| 'manual';
let parser: OutputFixingParser<object>;
if (schemaType === 'fromAttributes') {
const attributes = this.getNodeParameter(
'attributes.attributes',
0,
[],
) as AttributeDefinition[];
if (attributes.length === 0) {
throw new NodeOperationError(this.getNode(), 'At least one attribute must be specified');
}
parser = OutputFixingParser.fromLLM(
llm,
StructuredOutputParser.fromZodSchema(makeZodSchemaFromAttributes(attributes)),
);
} else {
let jsonSchema: JSONSchema7;
if (schemaType === 'fromJson') {
const jsonExample = this.getNodeParameter('jsonSchemaExample', 0, '') as string;
// Enforce all fields to be required in the generated schema if the node version is 1.2 or higher
const jsonExampleAllFieldsRequired = this.getNode().typeVersion >= 1.2;
jsonSchema = generateSchemaFromExample(jsonExample, jsonExampleAllFieldsRequired);
} else {
const inputSchema = this.getNodeParameter('inputSchema', 0, '') as string;
jsonSchema = jsonParse<JSONSchema7>(inputSchema);
}
const zodSchema = convertJsonSchemaToZod<z.ZodSchema<object>>(jsonSchema);
parser = OutputFixingParser.fromLLM(llm, StructuredOutputParser.fromZodSchema(zodSchema));
}
const resultData: INodeExecutionData[] = [];
const batchSize = this.getNodeParameter('options.batching.batchSize', 0, 5) as number;
const delayBetweenBatches = this.getNodeParameter(
'options.batching.delayBetweenBatches',
0,
0,
) as number;
if (this.getNode().typeVersion >= 1.1 && batchSize >= 1) {
// Batch processing
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(async (_item, batchItemIndex) => {
const itemIndex = i + batchItemIndex;
return await processItem(this, itemIndex, llm, parser);
});
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach((response, index) => {
if (response.status === 'rejected') {
const error = response.reason as Error;
if (this.continueOnFail()) {
resultData.push({
json: { error: error.message },
pairedItem: { item: i + index },
});
return;
} else {
throw new NodeOperationError(this.getNode(), error.message);
}
}
const output = response.value;
resultData.push({ json: { output } });
});
// Add delay between batches if not the last batch
if (i + batchSize < items.length && delayBetweenBatches > 0) {
await sleep(delayBetweenBatches);
}
}
} else {
// Sequential processing
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
const output = await processItem(this, itemIndex, llm, parser);
resultData.push({ json: { output } });
} catch (error) {
if (this.continueOnFail()) {
resultData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
continue;
}
throw error;
}
}
}
return [resultData];
}
}
@@ -0,0 +1,3 @@
export const SYSTEM_PROMPT_TEMPLATE = `You are an expert extraction algorithm.
Only extract relevant information from the text.
If you do not know the value of an attribute asked to extract, you may omit the attribute's value.`;
@@ -0,0 +1,34 @@
import { z } from 'zod';
import type { AttributeDefinition } from './types';
function makeAttributeSchema(attributeDefinition: AttributeDefinition, required: boolean = true) {
let schema: z.ZodTypeAny;
if (attributeDefinition.type === 'string') {
schema = z.string();
} else if (attributeDefinition.type === 'number') {
schema = z.number();
} else if (attributeDefinition.type === 'boolean') {
schema = z.boolean();
} else if (attributeDefinition.type === 'date') {
schema = z.string().date();
} else {
schema = z.unknown();
}
if (!required) {
schema = schema.optional();
}
return schema.describe(attributeDefinition.description);
}
export function makeZodSchemaFromAttributes(attributes: AttributeDefinition[]) {
const schemaEntries = attributes.map((attr) => [
attr.name,
makeAttributeSchema(attr, attr.required),
]);
return z.object(Object.fromEntries(schemaEntries));
}
@@ -0,0 +1,49 @@
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import { HumanMessage } from '@langchain/core/messages';
import { ChatPromptTemplate, SystemMessagePromptTemplate } from '@langchain/core/prompts';
import type { OutputFixingParser } from '@langchain/classic/output_parsers';
import { NodeOperationError, type IExecuteFunctions } from 'n8n-workflow';
import { getTracingConfig } from '@utils/tracing';
import { SYSTEM_PROMPT_TEMPLATE } from './constants';
export async function processItem(
ctx: IExecuteFunctions,
itemIndex: number,
llm: BaseLanguageModel,
parser: OutputFixingParser<object>,
) {
const input = ctx.getNodeParameter('text', itemIndex) as string;
if (!input?.trim()) {
throw new NodeOperationError(ctx.getNode(), `Text for item ${itemIndex} is not defined`, {
itemIndex,
});
}
const inputPrompt = new HumanMessage(input);
const options = ctx.getNodeParameter('options', itemIndex, {}) as {
systemPromptTemplate?: string;
};
const escapedTemplate = (options.systemPromptTemplate ?? SYSTEM_PROMPT_TEMPLATE).replace(
/[{}]/g,
(match) => match + match,
);
const systemPromptTemplate = SystemMessagePromptTemplate.fromTemplate(
`${escapedTemplate}
{format_instructions}`,
);
const messages = [
await systemPromptTemplate.format({
format_instructions: parser.getFormatInstructions(),
}),
inputPrompt,
];
const prompt = ChatPromptTemplate.fromMessages(messages);
const chain = prompt.pipe(llm).pipe(parser).withConfig(getTracingConfig(ctx));
return await chain.invoke(messages);
}
@@ -0,0 +1,420 @@
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import { FakeListChatModel } from '@langchain/core/utils/testing';
import { mock } from 'jest-mock-extended';
import get from 'lodash/get';
import type { IDataObject, IExecuteFunctions, INode } from 'n8n-workflow';
import { makeZodSchemaFromAttributes } from '../helpers';
import { InformationExtractor } from '../InformationExtractor.node';
import type { AttributeDefinition } from '../types';
const mockPersonAttributes: AttributeDefinition[] = [
{
name: 'name',
type: 'string',
description: 'The name of the person',
required: false,
},
{
name: 'age',
type: 'number',
description: 'The age of the person',
required: false,
},
];
const mockPersonAttributesRequired: AttributeDefinition[] = [
{
name: 'name',
type: 'string',
description: 'The name of the person',
required: true,
},
{
name: 'age',
type: 'number',
description: 'The age of the person',
required: true,
},
];
function formatFakeLlmResponse(object: Record<string, any>) {
return `\`\`\`json\n${JSON.stringify(object, null, 2)}\n\`\`\``;
}
const createExecuteFunctionsMock = (
parameters: IDataObject,
fakeLlm: BaseLanguageModel,
inputData = [{ json: {} }],
) => {
const nodeParameters = parameters;
return {
getNodeParameter(parameter: string) {
return get(nodeParameters, parameter);
},
getNode() {
return {
typeVersion: 1.1,
};
},
getInputConnectionData() {
return fakeLlm;
},
getInputData() {
return inputData;
},
getWorkflow() {
return {
name: 'Test Workflow',
};
},
getExecutionId() {
return 'test_execution_id';
},
continueOnFail() {
return false;
},
} as unknown as IExecuteFunctions;
};
describe('InformationExtractor', () => {
describe('Schema Generation', () => {
it('should generate a schema from attribute descriptions with optional fields', async () => {
const schema = makeZodSchemaFromAttributes(mockPersonAttributes);
expect(schema.parse({ name: 'John', age: 30 })).toEqual({ name: 'John', age: 30 });
expect(schema.parse({ name: 'John' })).toEqual({ name: 'John' });
expect(schema.parse({ age: 30 })).toEqual({ age: 30 });
});
});
describe('Single Item Processing with JSON Schema from Example', () => {
it('should extract information using JSON schema from example - version 1.2 (required fields)', async () => {
const node = new InformationExtractor();
const inputData = [
{
json: { text: 'John lives in California and has visited Los Angeles and San Francisco' },
},
];
const mockExecuteFunctions = createExecuteFunctionsMock(
{
text: 'John lives in California and has visited Los Angeles and San Francisco',
schemaType: 'fromJson',
jsonSchemaExample: JSON.stringify({
state: 'California',
cities: ['Los Angeles', 'San Francisco'],
}),
options: {
systemPromptTemplate: '',
},
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({
state: 'California',
cities: ['Los Angeles', 'San Francisco'],
}),
],
}),
inputData,
);
// Mock version 1.2 to test required fields behavior
mockExecuteFunctions.getNode = () => mock<INode>({ typeVersion: 1.2 });
const response = await node.execute.call(mockExecuteFunctions);
expect(response).toEqual([
[
{
json: {
output: {
state: 'California',
cities: ['Los Angeles', 'San Francisco'],
},
},
},
],
]);
});
it('should extract information using JSON schema from example - version 1.1 (optional fields)', async () => {
const node = new InformationExtractor();
const inputData = [{ json: { text: 'John lives in California' } }];
const mockExecuteFunctions = createExecuteFunctionsMock(
{
text: 'John lives in California',
schemaType: 'fromJson',
jsonSchemaExample: JSON.stringify({
state: 'California',
cities: ['Los Angeles', 'San Francisco'],
}),
options: {
systemPromptTemplate: '',
},
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({
state: 'California',
// cities field missing - should be allowed in v1.1
}),
],
}),
inputData,
);
// Mock version 1.1 to test optional fields behavior
mockExecuteFunctions.getNode = () => mock<INode>({ typeVersion: 1.1 });
const response = await node.execute.call(mockExecuteFunctions);
expect(response).toEqual([
[
{
json: {
output: {
state: 'California',
},
},
},
],
]);
});
it('should throw error for incomplete model output in version 1.2 (required fields)', async () => {
const node = new InformationExtractor();
const inputData = [{ json: { text: 'John lives in California' } }];
const mockExecuteFunctions = createExecuteFunctionsMock(
{
text: 'John lives in California',
schemaType: 'fromJson',
jsonSchemaExample: JSON.stringify({
state: 'California',
cities: ['Los Angeles', 'San Francisco'],
zipCode: '90210',
}),
options: {
systemPromptTemplate: '',
},
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({
state: 'California',
// Missing cities and zipCode - should fail in v1.2 since all fields are required
}),
],
}),
inputData,
);
mockExecuteFunctions.getNode = () => mock<INode>({ typeVersion: 1.2 });
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow();
});
it('should extract information using complex nested JSON schema from example', async () => {
const node = new InformationExtractor();
const inputData = [
{
json: {
text: 'John Doe works at Acme Corp as a Software Engineer with 5 years experience',
},
},
];
const complexSchema = {
person: {
name: 'John Doe',
company: {
name: 'Acme Corp',
position: 'Software Engineer',
},
},
experience: {
years: 5,
skills: ['JavaScript', 'TypeScript'],
},
};
const mockExecuteFunctions = createExecuteFunctionsMock(
{
text: 'John Doe works at Acme Corp as a Software Engineer with 5 years experience',
schemaType: 'fromJson',
jsonSchemaExample: JSON.stringify(complexSchema),
options: {
systemPromptTemplate: '',
},
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({
person: {
name: 'John Doe',
company: {
name: 'Acme Corp',
position: 'Software Engineer',
},
},
experience: {
years: 5,
skills: ['JavaScript', 'TypeScript'],
},
}),
],
}),
inputData,
);
mockExecuteFunctions.getNode = () => mock<INode>({ typeVersion: 1.2 });
const response = await node.execute.call(mockExecuteFunctions);
expect(response[0][0].json.output).toMatchObject({
person: {
name: 'John Doe',
company: {
name: 'Acme Corp',
position: 'Software Engineer',
},
},
experience: {
years: 5,
skills: expect.arrayContaining(['JavaScript', 'TypeScript']),
},
});
});
});
describe('Batch Processing', () => {
it('should process multiple items in batches', async () => {
const node = new InformationExtractor();
const inputData = [
{ json: { text: 'John is 30 years old' } },
{ json: { text: 'Alice is 25 years old' } },
{ json: { text: 'Bob is 40 years old' } },
];
const response = await node.execute.call(
createExecuteFunctionsMock(
{
text: 'John is 30 years old',
attributes: {
attributes: mockPersonAttributes,
},
options: {
batching: {
batchSize: 2,
delayBetweenBatches: 0,
},
},
schemaType: 'fromAttributes',
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({ name: 'John', age: 30 }),
formatFakeLlmResponse({ name: 'Alice', age: 25 }),
formatFakeLlmResponse({ name: 'Bob', age: 40 }),
],
}),
inputData,
),
);
expect(response).toEqual([
[
{ json: { output: { name: 'John', age: 30 } } },
{ json: { output: { name: 'Alice', age: 25 } } },
{ json: { output: { name: 'Bob', age: 40 } } },
],
]);
});
it('should handle errors in batch processing', async () => {
const node = new InformationExtractor();
const inputData = [
{ json: { text: 'John is 30 years old' } },
{ json: { text: 'Invalid text' } },
{ json: { text: 'Bob is 40 years old' } },
];
const mockExecuteFunctions = createExecuteFunctionsMock(
{
text: 'John is 30 years old',
attributes: {
attributes: mockPersonAttributesRequired,
},
options: {
batching: {
batchSize: 2,
delayBetweenBatches: 0,
},
},
schemaType: 'fromAttributes',
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({ name: 'John', age: 30 }),
formatFakeLlmResponse({ name: 'Invalid' }), // Missing required age
formatFakeLlmResponse({ name: 'Invalid' }), // Missing required age on retry
formatFakeLlmResponse({ name: 'Bob', age: 40 }),
],
}),
inputData,
);
mockExecuteFunctions.continueOnFail = () => true;
const response = await node.execute.call(mockExecuteFunctions);
expect(response[0]).toHaveLength(3);
expect(response[0][0]).toEqual({ json: { output: { name: 'John', age: 30 } } });
expect(response[0][1]).toEqual({
json: { error: expect.stringContaining('Failed to parse') },
pairedItem: { item: 1 },
});
expect(response[0][2]).toEqual({ json: { output: { name: 'Bob', age: 40 } } });
});
it('should throw error if batch processing fails and continueOnFail is false', async () => {
const node = new InformationExtractor();
const inputData = [
{ json: { text: 'John is 30 years old' } },
{ json: { text: 'Invalid text' } },
{ json: { text: 'Bob is 40 years old' } },
];
const mockExecuteFunctions = createExecuteFunctionsMock(
{
text: 'John is 30 years old',
attributes: {
attributes: mockPersonAttributesRequired,
},
options: {
batching: {
batchSize: 2,
delayBetweenBatches: 0,
},
},
schemaType: 'fromAttributes',
},
new FakeListChatModel({
responses: [
formatFakeLlmResponse({ name: 'John', age: 30 }),
formatFakeLlmResponse({ name: 'Invalid' }), // Missing required age
formatFakeLlmResponse({ name: 'Invalid' }), // Missing required age on retry
formatFakeLlmResponse({ name: 'Bob', age: 40 }),
],
}),
inputData,
);
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow('Failed to parse');
});
});
});
@@ -0,0 +1,168 @@
import { FakeLLM, FakeListChatModel } from '@langchain/core/utils/testing';
import { OutputFixingParser, StructuredOutputParser } from '@langchain/classic/output_parsers';
import { NodeOperationError } from 'n8n-workflow';
import { makeZodSchemaFromAttributes } from '../helpers';
import { processItem } from '../processItem';
import type { AttributeDefinition } from '../types';
jest.mock('@utils/tracing', () => ({
getTracingConfig: () => ({}),
}));
const mockPersonAttributes: AttributeDefinition[] = [
{
name: 'name',
type: 'string',
description: 'The name of the person',
required: false,
},
{
name: 'age',
type: 'number',
description: 'The age of the person',
required: false,
},
];
const mockPersonAttributesRequired: AttributeDefinition[] = [
{
name: 'name',
type: 'string',
description: 'The name of the person',
required: true,
},
{
name: 'age',
type: 'number',
description: 'The age of the person',
required: true,
},
];
function formatFakeLlmResponse(object: Record<string, any>) {
return `\`\`\`json\n${JSON.stringify(object, null, 2)}\n\`\`\``;
}
describe('processItem', () => {
it('should process a single item and return extracted attributes', async () => {
const mockExecuteFunctions = {
getNodeParameter: (param: string) => {
if (param === 'text') return 'John is 30 years old';
if (param === 'options') return {};
return undefined;
},
getNode: () => ({ typeVersion: 1.1 }),
};
const llm = new FakeLLM({ response: formatFakeLlmResponse({ name: 'John', age: 30 }) });
const parser = OutputFixingParser.fromLLM(
llm,
StructuredOutputParser.fromZodSchema(makeZodSchemaFromAttributes(mockPersonAttributes)),
);
const result = await processItem(mockExecuteFunctions as any, 0, llm, parser);
expect(result).toEqual({ name: 'John', age: 30 });
});
it('should throw error if input is undefined or empty', async () => {
const mockExecuteFunctions = {
getNodeParameter: (param: string, itemIndex: number) => {
if (param === 'text') {
if (itemIndex === 0) return undefined;
if (itemIndex === 1) return '';
if (itemIndex === 2) return ' ';
return null;
}
if (param === 'options') return {};
return undefined;
},
getNode: () => ({ typeVersion: 1.1 }),
};
const llm = new FakeLLM({ response: formatFakeLlmResponse({ name: 'John', age: 30 }) });
const parser = OutputFixingParser.fromLLM(
llm,
StructuredOutputParser.fromZodSchema(makeZodSchemaFromAttributes(mockPersonAttributes)),
);
for (let itemIndex = 0; itemIndex < 4; itemIndex++) {
await expect(
processItem(mockExecuteFunctions as any, itemIndex, llm, parser),
).rejects.toThrow(NodeOperationError);
}
});
it('should use custom system prompt template if provided', async () => {
const customTemplate = 'Custom template {format_instructions}';
const mockExecuteFunctions = {
getNodeParameter: (param: string) => {
if (param === 'text') return 'John is 30 years old';
if (param === 'options') return { systemPromptTemplate: customTemplate };
return undefined;
},
getNode: () => ({ typeVersion: 1.1 }),
};
const llm = new FakeLLM({ response: formatFakeLlmResponse({ name: 'John', age: 30 }) });
const parser = OutputFixingParser.fromLLM(
llm,
StructuredOutputParser.fromZodSchema(makeZodSchemaFromAttributes(mockPersonAttributes)),
);
const result = await processItem(mockExecuteFunctions as any, 0, llm, parser);
expect(result).toEqual({ name: 'John', age: 30 });
});
it('should handle curly braces in custom system prompt template', async () => {
const customTemplate = 'Extract JSON like this: {"name": "value"} from the text.';
const mockExecuteFunctions = {
getNodeParameter: (param: string) => {
if (param === 'text') return 'John is 30 years old';
if (param === 'options') return { systemPromptTemplate: customTemplate };
return undefined;
},
getNode: () => ({ typeVersion: 1.1 }),
};
const llm = new FakeLLM({ response: formatFakeLlmResponse({ name: 'John', age: 30 }) });
const parser = OutputFixingParser.fromLLM(
llm,
StructuredOutputParser.fromZodSchema(makeZodSchemaFromAttributes(mockPersonAttributes)),
);
const result = await processItem(mockExecuteFunctions as any, 0, llm, parser);
expect(result).toEqual({ name: 'John', age: 30 });
});
it('should handle retries when LLM returns invalid data', async () => {
const mockExecuteFunctions = {
getNodeParameter: (param: string) => {
if (param === 'text') return 'John is 30 years old';
if (param === 'options') return {};
return undefined;
},
getNode: () => ({ typeVersion: 1.1 }),
};
const llm = new FakeListChatModel({
responses: [
formatFakeLlmResponse({ name: 'John', age: '30' }), // Wrong type
formatFakeLlmResponse({ name: 'John', age: 30 }), // Correct type
],
});
const parser = OutputFixingParser.fromLLM(
llm,
StructuredOutputParser.fromZodSchema(
makeZodSchemaFromAttributes(mockPersonAttributesRequired),
),
);
const result = await processItem(mockExecuteFunctions as any, 0, llm, parser);
expect(result).toEqual({ name: 'John', age: 30 });
});
});
@@ -0,0 +1,6 @@
export interface AttributeDefinition {
name: string;
description: string;
type: 'string' | 'number' | 'boolean' | 'date';
required: boolean;
}