mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 03:12:15 +00:00
fix(Basic LLM Chain Node): Prevent stringifying of structured output on previous versions (#14200)
This commit is contained in:
@@ -116,10 +116,13 @@ export class ChainLlm implements INodeType {
|
|||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the node version is 1.6(and LLM is using `response_format: json_object`) or higher or an output parser is configured,
|
||||||
|
// we unwrap the response and return the object directly as JSON
|
||||||
|
const shouldUnwrapObjects = this.getNode().typeVersion >= 1.6 || !!outputParser;
|
||||||
// Process each response and add to return data
|
// Process each response and add to return data
|
||||||
responses.forEach((response) => {
|
responses.forEach((response) => {
|
||||||
returnData.push({
|
returnData.push({
|
||||||
json: formatResponse(response, this.getNode().typeVersion),
|
json: formatResponse(response, shouldUnwrapObjects),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ import { getTracingConfig } from '@utils/tracing';
|
|||||||
import { createPromptTemplate } from './promptUtils';
|
import { createPromptTemplate } from './promptUtils';
|
||||||
import type { ChainExecutionParams } from './types';
|
import type { ChainExecutionParams } from './types';
|
||||||
|
|
||||||
|
export class NaiveJsonOutputParser<
|
||||||
|
T extends Record<string, any> = Record<string, any>,
|
||||||
|
> extends JsonOutputParser<T> {
|
||||||
|
async parse(text: string): Promise<T> {
|
||||||
|
// First try direct JSON parsing
|
||||||
|
try {
|
||||||
|
const directParsed = JSON.parse(text);
|
||||||
|
return directParsed as T;
|
||||||
|
} catch (e) {
|
||||||
|
// If fails, fall back to JsonOutputParser parser
|
||||||
|
return await super.parse(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard to check if the LLM has a modelKwargs property(OpenAI)
|
* Type guard to check if the LLM has a modelKwargs property(OpenAI)
|
||||||
*/
|
*/
|
||||||
@@ -39,11 +54,11 @@ export function getOutputParserForLLM(
|
|||||||
llm: BaseLanguageModel,
|
llm: BaseLanguageModel,
|
||||||
): BaseLLMOutputParser<string | Record<string, unknown>> {
|
): BaseLLMOutputParser<string | Record<string, unknown>> {
|
||||||
if (isModelWithResponseFormat(llm) && llm.modelKwargs?.response_format?.type === 'json_object') {
|
if (isModelWithResponseFormat(llm) && llm.modelKwargs?.response_format?.type === 'json_object') {
|
||||||
return new JsonOutputParser();
|
return new NaiveJsonOutputParser();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isModelWithFormat(llm) && llm.format === 'json') {
|
if (isModelWithFormat(llm) && llm.format === 'json') {
|
||||||
return new JsonOutputParser();
|
return new NaiveJsonOutputParser();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StringOutputParser();
|
return new StringOutputParser();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { IDataObject } from 'n8n-workflow';
|
|||||||
/**
|
/**
|
||||||
* Formats the response from the LLM chain into a consistent structure
|
* Formats the response from the LLM chain into a consistent structure
|
||||||
*/
|
*/
|
||||||
export function formatResponse(response: unknown, version: number): IDataObject {
|
export function formatResponse(response: unknown, returnUnwrappedObject: boolean): IDataObject {
|
||||||
if (typeof response === 'string') {
|
if (typeof response === 'string') {
|
||||||
return {
|
return {
|
||||||
text: response.trim(),
|
text: response.trim(),
|
||||||
@@ -17,10 +17,12 @@ export function formatResponse(response: unknown, version: number): IDataObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response instanceof Object) {
|
if (response instanceof Object) {
|
||||||
if (version >= 1.6) {
|
if (returnUnwrappedObject) {
|
||||||
return response as IDataObject;
|
return response as IDataObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the response is an object and we are not unwrapping it, we need to stringify it
|
||||||
|
// to be backwards compatible with older versions of the chain(< 1.6)
|
||||||
return {
|
return {
|
||||||
text: JSON.stringify(response),
|
text: JSON.stringify(response),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as outputParserModule from '@utils/output_parsers/N8nOutputParser';
|
|||||||
|
|
||||||
import { ChainLlm } from '../ChainLlm.node';
|
import { ChainLlm } from '../ChainLlm.node';
|
||||||
import * as executeChainModule from '../methods/chainExecutor';
|
import * as executeChainModule from '../methods/chainExecutor';
|
||||||
|
import * as responseFormatterModule from '../methods/responseFormatter';
|
||||||
|
|
||||||
jest.mock('@utils/helpers', () => ({
|
jest.mock('@utils/helpers', () => ({
|
||||||
getPromptInputByType: jest.fn(),
|
getPromptInputByType: jest.fn(),
|
||||||
@@ -23,6 +24,15 @@ jest.mock('../methods/chainExecutor', () => ({
|
|||||||
executeChain: jest.fn(),
|
executeChain: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../methods/responseFormatter', () => ({
|
||||||
|
formatResponse: jest.fn().mockImplementation((response) => {
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
return { text: response.trim() };
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('ChainLlm Node', () => {
|
describe('ChainLlm Node', () => {
|
||||||
let node: ChainLlm;
|
let node: ChainLlm;
|
||||||
let mockExecuteFunction: jest.Mocked<IExecuteFunctions>;
|
let mockExecuteFunction: jest.Mocked<IExecuteFunctions>;
|
||||||
@@ -93,7 +103,6 @@ describe('ChainLlm Node', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple input items', async () => {
|
it('should handle multiple input items', async () => {
|
||||||
// Set up multiple input items
|
|
||||||
mockExecuteFunction.getInputData.mockReturnValue([
|
mockExecuteFunction.getInputData.mockReturnValue([
|
||||||
{ json: { item: 1 } },
|
{ json: { item: 1 } },
|
||||||
{ json: { item: 2 } },
|
{ json: { item: 2 } },
|
||||||
@@ -112,12 +121,10 @@ describe('ChainLlm Node', () => {
|
|||||||
const result = await node.execute.call(mockExecuteFunction);
|
const result = await node.execute.call(mockExecuteFunction);
|
||||||
|
|
||||||
expect(executeChainModule.executeChain).toHaveBeenCalledTimes(2);
|
expect(executeChainModule.executeChain).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(result[0]).toHaveLength(2);
|
expect(result[0]).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the prompt parameter directly for older versions', async () => {
|
it('should use the prompt parameter directly for older versions', async () => {
|
||||||
// Set an older version
|
|
||||||
mockExecuteFunction.getNode.mockReturnValue({
|
mockExecuteFunction.getNode.mockReturnValue({
|
||||||
name: 'Chain LLM',
|
name: 'Chain LLM',
|
||||||
typeVersion: 1.3,
|
typeVersion: 1.3,
|
||||||
@@ -183,5 +190,173 @@ describe('ChainLlm Node', () => {
|
|||||||
|
|
||||||
expect(result[0]).toHaveLength(2);
|
expect(result[0]).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should unwrap object responses when node version is 1.6 or higher', async () => {
|
||||||
|
mockExecuteFunction.getNode.mockReturnValue({
|
||||||
|
name: 'Chain LLM',
|
||||||
|
typeVersion: 1.6,
|
||||||
|
parameters: {},
|
||||||
|
} as INode);
|
||||||
|
|
||||||
|
(helperModule.getPromptInputByType as jest.Mock).mockReturnValue('Test prompt');
|
||||||
|
(outputParserModule.getOptionalOutputParser as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const structuredResponse = {
|
||||||
|
person: { name: 'John', age: 30 },
|
||||||
|
items: ['item1', 'item2'],
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
(executeChainModule.executeChain as jest.Mock).mockResolvedValue([structuredResponse]);
|
||||||
|
|
||||||
|
const formatResponseSpy = jest.spyOn(responseFormatterModule, 'formatResponse');
|
||||||
|
|
||||||
|
await node.execute.call(mockExecuteFunction);
|
||||||
|
|
||||||
|
expect(formatResponseSpy).toHaveBeenCalledWith(structuredResponse, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unwrap object responses when output parser is provided regardless of version', async () => {
|
||||||
|
mockExecuteFunction.getNode.mockReturnValue({
|
||||||
|
name: 'Chain LLM',
|
||||||
|
typeVersion: 1.5,
|
||||||
|
parameters: {},
|
||||||
|
} as INode);
|
||||||
|
|
||||||
|
(helperModule.getPromptInputByType as jest.Mock).mockReturnValue('Test prompt');
|
||||||
|
|
||||||
|
(outputParserModule.getOptionalOutputParser as jest.Mock).mockResolvedValue(
|
||||||
|
mock<outputParserModule.N8nOutputParser>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const structuredResponse = {
|
||||||
|
result: 'success',
|
||||||
|
data: { key: 'value' },
|
||||||
|
};
|
||||||
|
|
||||||
|
(executeChainModule.executeChain as jest.Mock).mockResolvedValue([structuredResponse]);
|
||||||
|
|
||||||
|
const formatResponseSpy = jest.spyOn(responseFormatterModule, 'formatResponse');
|
||||||
|
|
||||||
|
await node.execute.call(mockExecuteFunction);
|
||||||
|
|
||||||
|
expect(formatResponseSpy).toHaveBeenCalledWith(structuredResponse, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap object responses as text when node version is lower than 1.6 and no output parser', async () => {
|
||||||
|
mockExecuteFunction.getNode.mockReturnValue({
|
||||||
|
name: 'Chain LLM',
|
||||||
|
typeVersion: 1.5,
|
||||||
|
parameters: {},
|
||||||
|
} as INode);
|
||||||
|
|
||||||
|
(helperModule.getPromptInputByType as jest.Mock).mockReturnValue('Test prompt');
|
||||||
|
(outputParserModule.getOptionalOutputParser as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const structuredResponse = {
|
||||||
|
person: { name: 'John', age: 30 },
|
||||||
|
items: ['item1', 'item2'],
|
||||||
|
};
|
||||||
|
|
||||||
|
(executeChainModule.executeChain as jest.Mock).mockResolvedValue([structuredResponse]);
|
||||||
|
|
||||||
|
const formatResponseSpy = jest.spyOn(responseFormatterModule, 'formatResponse');
|
||||||
|
|
||||||
|
await node.execute.call(mockExecuteFunction);
|
||||||
|
|
||||||
|
expect(formatResponseSpy).toHaveBeenCalledWith(structuredResponse, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a mix of different response types with the correct wrapping', async () => {
|
||||||
|
mockExecuteFunction.getNode.mockReturnValue({
|
||||||
|
name: 'Chain LLM',
|
||||||
|
typeVersion: 1.6,
|
||||||
|
parameters: {},
|
||||||
|
} as INode);
|
||||||
|
|
||||||
|
(helperModule.getPromptInputByType as jest.Mock).mockReturnValue('Test prompt');
|
||||||
|
(outputParserModule.getOptionalOutputParser as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const mixedResponses = ['Text response', { structured: 'object' }, ['array', 'response']];
|
||||||
|
|
||||||
|
(executeChainModule.executeChain as jest.Mock).mockResolvedValue(mixedResponses);
|
||||||
|
|
||||||
|
(responseFormatterModule.formatResponse as jest.Mock).mockClear();
|
||||||
|
|
||||||
|
await node.execute.call(mockExecuteFunction);
|
||||||
|
|
||||||
|
expect(responseFormatterModule.formatResponse).toHaveBeenCalledTimes(3);
|
||||||
|
expect(responseFormatterModule.formatResponse).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'Text response',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(responseFormatterModule.formatResponse).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
{ structured: 'object' },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(responseFormatterModule.formatResponse).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
['array', 'response'],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle LLM responses containing JSON with markdown content', async () => {
|
||||||
|
mockExecuteFunction.getNode.mockReturnValue({
|
||||||
|
name: 'Chain LLM',
|
||||||
|
typeVersion: 1.6,
|
||||||
|
parameters: {},
|
||||||
|
} as INode);
|
||||||
|
|
||||||
|
(helperModule.getPromptInputByType as jest.Mock).mockReturnValue(
|
||||||
|
'Generate markdown documentation',
|
||||||
|
);
|
||||||
|
(outputParserModule.getOptionalOutputParser as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const markdownResponse = {
|
||||||
|
title: 'API Documentation',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Authentication',
|
||||||
|
content:
|
||||||
|
"# Authentication\n\nUse API keys for all requests:\n\n```javascript\nconst headers = {\n 'Authorization': 'Bearer YOUR_API_KEY'\n};\n```",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Endpoints',
|
||||||
|
content:
|
||||||
|
'## Available Endpoints\n\n* GET /users - List all users\n* POST /users - Create a user\n* GET /users/{id} - Get user details',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
examples: {
|
||||||
|
curl: "```bash\ncurl -X GET https://api.example.com/users \\\n -H 'Authorization: Bearer YOUR_API_KEY'\n```",
|
||||||
|
response: '```json\n{\n "users": [],\n "count": 0\n}\n```',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
(executeChainModule.executeChain as jest.Mock).mockResolvedValue([markdownResponse]);
|
||||||
|
|
||||||
|
(responseFormatterModule.formatResponse as jest.Mock).mockImplementation(
|
||||||
|
(response, shouldUnwrap) => {
|
||||||
|
if (shouldUnwrap && typeof response === 'object') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return { text: JSON.stringify(response) };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await node.execute.call(mockExecuteFunction);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: markdownResponse,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(responseFormatterModule.formatResponse).toHaveBeenCalledWith(markdownResponse, true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { IExecuteFunctions } from 'n8n-workflow';
|
|||||||
import type { N8nOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
import type { N8nOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
||||||
import * as tracing from '@utils/tracing';
|
import * as tracing from '@utils/tracing';
|
||||||
|
|
||||||
import { executeChain } from '../methods/chainExecutor';
|
import { executeChain, NaiveJsonOutputParser } from '../methods/chainExecutor';
|
||||||
import * as chainExecutor from '../methods/chainExecutor';
|
import * as chainExecutor from '../methods/chainExecutor';
|
||||||
import * as promptUtils from '../methods/promptUtils';
|
import * as promptUtils from '../methods/promptUtils';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ describe('chainExecutor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getOutputParserForLLM', () => {
|
describe('getOutputParserForLLM', () => {
|
||||||
it('should return JsonOutputParser for OpenAI-like models with json_object response format', () => {
|
it('should return NaiveJsonOutputParser for OpenAI-like models with json_object response format', () => {
|
||||||
const openAILikeModel = {
|
const openAILikeModel = {
|
||||||
modelKwargs: {
|
modelKwargs: {
|
||||||
response_format: {
|
response_format: {
|
||||||
@@ -42,10 +42,10 @@ describe('chainExecutor', () => {
|
|||||||
const parser = chainExecutor.getOutputParserForLLM(
|
const parser = chainExecutor.getOutputParserForLLM(
|
||||||
openAILikeModel as unknown as BaseChatModel,
|
openAILikeModel as unknown as BaseChatModel,
|
||||||
);
|
);
|
||||||
expect(parser).toBeInstanceOf(JsonOutputParser);
|
expect(parser).toBeInstanceOf(NaiveJsonOutputParser);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return JsonOutputParser for Ollama models with json format', () => {
|
it('should return NaiveJsonOutputParser for Ollama models with json format', () => {
|
||||||
const ollamaLikeModel = {
|
const ollamaLikeModel = {
|
||||||
format: 'json',
|
format: 'json',
|
||||||
};
|
};
|
||||||
@@ -53,7 +53,7 @@ describe('chainExecutor', () => {
|
|||||||
const parser = chainExecutor.getOutputParserForLLM(
|
const parser = chainExecutor.getOutputParserForLLM(
|
||||||
ollamaLikeModel as unknown as BaseChatModel,
|
ollamaLikeModel as unknown as BaseChatModel,
|
||||||
);
|
);
|
||||||
expect(parser).toBeInstanceOf(JsonOutputParser);
|
expect(parser).toBeInstanceOf(NaiveJsonOutputParser);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return StringOutputParser for models without JSON format settings', () => {
|
it('should return StringOutputParser for models without JSON format settings', () => {
|
||||||
@@ -64,6 +64,121 @@ describe('chainExecutor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('NaiveJsonOutputParser', () => {
|
||||||
|
it('should parse valid JSON directly', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const jsonStr = '{"name": "John", "age": 30}';
|
||||||
|
|
||||||
|
const result = await parser.parse(jsonStr);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: 'John',
|
||||||
|
age: 30,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested JSON objects', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const jsonStr = '{"person": {"name": "John", "age": 30}, "active": true}';
|
||||||
|
|
||||||
|
const result = await parser.parse(jsonStr);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
person: {
|
||||||
|
name: 'John',
|
||||||
|
age: 30,
|
||||||
|
},
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use parent class parser for malformed JSON', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const superParseSpy = jest.spyOn(JsonOutputParser.prototype, 'parse').mockResolvedValue({
|
||||||
|
parsed: 'content',
|
||||||
|
});
|
||||||
|
|
||||||
|
const malformedJson = 'Sure, here is your JSON: {"name": "John", "age": 30}';
|
||||||
|
|
||||||
|
await parser.parse(malformedJson);
|
||||||
|
|
||||||
|
expect(superParseSpy).toHaveBeenCalledWith(malformedJson);
|
||||||
|
superParseSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON with surrounding text by using parent parser', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const jsonWithText = 'Here is the result: {"result": "success", "code": 200}';
|
||||||
|
|
||||||
|
// Mock the parent class parse method
|
||||||
|
const mockParsedResult = { result: 'success', code: 200 };
|
||||||
|
const superParseSpy = jest
|
||||||
|
.spyOn(JsonOutputParser.prototype, 'parse')
|
||||||
|
.mockResolvedValue(mockParsedResult);
|
||||||
|
|
||||||
|
const result = await parser.parse(jsonWithText);
|
||||||
|
|
||||||
|
expect(superParseSpy).toHaveBeenCalledWith(jsonWithText);
|
||||||
|
expect(result).toEqual(mockParsedResult);
|
||||||
|
|
||||||
|
superParseSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly parse JSON with markdown text inside properties', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const jsonWithMarkdown = `{
|
||||||
|
"title": "Markdown Guide",
|
||||||
|
"content": "# Heading 1\\n## Heading 2\\n* Bullet point\\n* Another bullet\\n\\n\`\`\`code block\`\`\`\\n> Blockquote",
|
||||||
|
"description": "A guide with **bold** and *italic* text"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await parser.parse(jsonWithMarkdown);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: 'Markdown Guide',
|
||||||
|
content:
|
||||||
|
'# Heading 1\n## Heading 2\n* Bullet point\n* Another bullet\n\n```code block```\n> Blockquote',
|
||||||
|
description: 'A guide with **bold** and *italic* text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly parse JSON with markdown code blocks containing JSON', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const jsonWithMarkdownAndNestedJson = `{
|
||||||
|
"title": "JSON Examples",
|
||||||
|
"examples": "Here's an example of JSON: \`\`\`json\\n{\\"nested\\": \\"json\\", \\"in\\": \\"code block\\"}\\n\`\`\`",
|
||||||
|
"valid": true
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await parser.parse(jsonWithMarkdownAndNestedJson);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: 'JSON Examples',
|
||||||
|
examples:
|
||||||
|
'Here\'s an example of JSON: ```json\n{"nested": "json", "in": "code block"}\n```',
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON with special characters in markdown content', async () => {
|
||||||
|
const parser = new NaiveJsonOutputParser();
|
||||||
|
const jsonWithSpecialChars = `{
|
||||||
|
"title": "Special Characters",
|
||||||
|
"content": "# Testing \\n\\n * List with **bold** & *italic*\\n * Item with [link](https://example.com)\\n * Math: 2 < 3 > 1 && true || false",
|
||||||
|
"technical": "function test() { return x < y && z > w; }"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await parser.parse(jsonWithSpecialChars);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: 'Special Characters',
|
||||||
|
content:
|
||||||
|
'# Testing \n\n * List with **bold** & *italic*\n * Item with [link](https://example.com)\n * Math: 2 < 3 > 1 && true || false',
|
||||||
|
technical: 'function test() { return x < y && z > w; }',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('executeChain', () => {
|
describe('executeChain', () => {
|
||||||
it('should execute a simple chain without output parsers', async () => {
|
it('should execute a simple chain without output parsers', async () => {
|
||||||
const fakeLLM = new FakeLLM({ response: 'Test response' });
|
const fakeLLM = new FakeLLM({ response: 'Test response' });
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { formatResponse } from '../methods/responseFormatter';
|
|||||||
describe('responseFormatter', () => {
|
describe('responseFormatter', () => {
|
||||||
describe('formatResponse', () => {
|
describe('formatResponse', () => {
|
||||||
it('should format string responses', () => {
|
it('should format string responses', () => {
|
||||||
const result = formatResponse('Test response', 1.6);
|
const result = formatResponse('Test response', true);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: 'Test response',
|
text: 'Test response',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trim string responses', () => {
|
it('should trim string responses', () => {
|
||||||
const result = formatResponse(' Test response with whitespace ', 1.6);
|
const result = formatResponse(' Test response with whitespace ', true);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: 'Test response with whitespace',
|
text: 'Test response with whitespace',
|
||||||
});
|
});
|
||||||
@@ -18,24 +18,70 @@ describe('responseFormatter', () => {
|
|||||||
|
|
||||||
it('should handle array responses', () => {
|
it('should handle array responses', () => {
|
||||||
const testArray = [{ item: 1 }, { item: 2 }];
|
const testArray = [{ item: 1 }, { item: 2 }];
|
||||||
const result = formatResponse(testArray, 1.6);
|
const result = formatResponse(testArray, true);
|
||||||
expect(result).toEqual({ data: testArray });
|
expect(result).toEqual({ data: testArray });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle object responses', () => {
|
it('should handle object responses when unwrapping is enabled', () => {
|
||||||
const testObject = { key: 'value', nested: { key: 'value' } };
|
const testObject = { key: 'value', nested: { key: 'value' } };
|
||||||
const result = formatResponse(testObject, 1.6);
|
const result = formatResponse(testObject, true);
|
||||||
expect(result).toEqual(testObject);
|
expect(result).toEqual(testObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should stringify object responses when unwrapping is disabled', () => {
|
||||||
|
const testObject = { key: 'value', nested: { key: 'value' } };
|
||||||
|
const result = formatResponse(testObject, false);
|
||||||
|
expect(result).toEqual({
|
||||||
|
text: JSON.stringify(testObject),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle primitive non-string responses', () => {
|
it('should handle primitive non-string responses', () => {
|
||||||
const testNumber = 42;
|
const testNumber = 42;
|
||||||
const result = formatResponse(testNumber, 1.6);
|
const result = formatResponse(testNumber, true);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
response: {
|
response: {
|
||||||
text: 42,
|
text: 42,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle complex object structures when unwrapping is enabled', () => {
|
||||||
|
const complexObject = {
|
||||||
|
person: {
|
||||||
|
name: 'John',
|
||||||
|
age: 30,
|
||||||
|
address: {
|
||||||
|
street: '123 Main St',
|
||||||
|
city: 'Anytown',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: [1, 2, 3],
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formatResponse(complexObject, true);
|
||||||
|
expect(result).toEqual(complexObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stringify complex object structures when unwrapping is disabled', () => {
|
||||||
|
const complexObject = {
|
||||||
|
person: {
|
||||||
|
name: 'John',
|
||||||
|
age: 30,
|
||||||
|
address: {
|
||||||
|
street: '123 Main St',
|
||||||
|
city: 'Anytown',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: [1, 2, 3],
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formatResponse(complexObject, false);
|
||||||
|
expect(result).toEqual({
|
||||||
|
text: JSON.stringify(complexObject),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user