mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user