fix(Structured Output Parser Node, Auto-fixing Output Parser Node, Tools Agent Node): Structured output improvements (#13908)

Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
Eugene
2025-03-13 18:20:24 +03:00
committed by GitHub
parent 31037484a5
commit 5b6b78709e
7 changed files with 138 additions and 13 deletions

View File

@@ -221,3 +221,22 @@ export const getConnectedTools = async (
return finalTools;
};
/**
* Sometimes model output is wrapped in an additional object property.
* This function unwraps the output if it is in the format { output: { output: { ... } } }
*/
export function unwrapNestedOutput(output: Record<string, unknown>): Record<string, unknown> {
if (
'output' in output &&
Object.keys(output).length === 1 &&
typeof output.output === 'object' &&
output.output !== null &&
'output' in output.output &&
Object.keys(output.output).length === 1
) {
return output.output as Record<string, unknown>;
}
return output;
}

View File

@@ -39,7 +39,12 @@ export class N8nOutputFixingParser extends BaseOutputParser {
try {
// First attempt to parse the completion
const response = await this.outputParser.parse(completion, callbacks, (e) => e);
const response = await this.outputParser.parse(completion, callbacks, (e) => {
if (e instanceof OutputParserException) {
return e;
}
return new OutputParserException(e.message, completion);
});
logAiEvent(this.context, 'ai-output-parsed', { text: completion, response });
this.context.addOutputData(NodeConnectionType.AiOutputParser, index, [

View File

@@ -5,7 +5,7 @@ import type { ISupplyDataFunctions } from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { z } from 'zod';
import { logAiEvent } from '../helpers';
import { logAiEvent, unwrapNestedOutput } from '../helpers';
const STRUCTURED_OUTPUT_KEY = '__structured__output';
const STRUCTURED_OUTPUT_OBJECT_KEY = '__structured__output__object';
@@ -36,11 +36,14 @@ export class N8nStructuredOutputParser extends StructuredOutputParser<
const json = JSON.parse(jsonString.trim());
const parsed = await this.schema.parseAsync(json);
const result = (get(parsed, [STRUCTURED_OUTPUT_KEY, STRUCTURED_OUTPUT_OBJECT_KEY]) ??
let result = (get(parsed, [STRUCTURED_OUTPUT_KEY, STRUCTURED_OUTPUT_OBJECT_KEY]) ??
get(parsed, [STRUCTURED_OUTPUT_KEY, STRUCTURED_OUTPUT_ARRAY_KEY]) ??
get(parsed, STRUCTURED_OUTPUT_KEY) ??
parsed) as Record<string, unknown>;
// Unwrap any doubly-nested output structures (e.g., {output: {output: {...}}})
result = unwrapNestedOutput(result);
logAiEvent(this.context, 'ai-output-parsed', { text, response: result });
this.context.addOutputData(NodeConnectionType.AiOutputParser, index, [

View File

@@ -4,7 +4,7 @@ import { NodeOperationError } from 'n8n-workflow';
import type { ISupplyDataFunctions, IExecuteFunctions, INode } from 'n8n-workflow';
import { z } from 'zod';
import { escapeSingleCurlyBrackets, getConnectedTools } from '../helpers';
import { escapeSingleCurlyBrackets, getConnectedTools, unwrapNestedOutput } from '../helpers';
import { N8nTool } from '../N8nTool';
describe('escapeSingleCurlyBrackets', () => {
@@ -243,3 +243,101 @@ describe('getConnectedTools', () => {
expect(tools[0]).toBe(mockN8nTool);
});
});
describe('unwrapNestedOutput', () => {
it('should unwrap doubly nested output', () => {
const input = {
output: {
output: {
text: 'Hello world',
confidence: 0.95,
},
},
};
const expected = {
output: {
text: 'Hello world',
confidence: 0.95,
},
};
expect(unwrapNestedOutput(input)).toEqual(expected);
});
it('should not modify regular output object', () => {
const input = {
output: {
text: 'Hello world',
confidence: 0.95,
},
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
it('should not modify object without output property', () => {
const input = {
result: 'success',
data: {
text: 'Hello world',
},
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
it('should not modify when output is not an object', () => {
const input = {
output: 'Hello world',
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
it('should not modify when object has multiple properties', () => {
const input = {
output: {
output: {
text: 'Hello world',
},
},
meta: {
timestamp: 123456789,
},
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
it('should not modify when inner output has multiple properties', () => {
const input = {
output: {
output: {
text: 'Hello world',
},
meta: {
timestamp: 123456789,
},
},
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
it('should handle null values properly', () => {
const input = {
output: null,
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
it('should handle empty object values properly', () => {
const input = {
output: {},
};
expect(unwrapNestedOutput(input)).toEqual(input);
});
});