feat: Add AI Error Debugging using OpenAI (#8805)

This commit is contained in:
Alex Grozav
2024-03-13 16:48:00 +02:00
committed by GitHub
parent e3dd353ea7
commit 948c383999
26 changed files with 1838 additions and 362 deletions

View File

@@ -0,0 +1,84 @@
import type { INode, INodeType } from 'n8n-workflow';
import { ApplicationError, NodeOperationError } from 'n8n-workflow';
import { AIService } from '@/services/ai.service';
import config from '@/config';
import { createDebugErrorPrompt } from '@/services/ai/prompts/debugError';
jest.mock('@/config', () => {
return {
getEnv: jest.fn().mockReturnValue('openai'),
};
});
jest.mock('@/services/ai/providers/openai', () => {
return {
AIProviderOpenAI: jest.fn().mockImplementation(() => {
return {
prompt: jest.fn(),
};
}),
};
});
describe('AIService', () => {
describe('constructor', () => {
test('should throw if prompting with unknown provider type', async () => {
jest.mocked(config).getEnv.mockReturnValue('unknown');
const aiService = new AIService();
await expect(async () => await aiService.prompt([])).rejects.toThrow(ApplicationError);
});
test('should throw if prompting with known provider type without api key', async () => {
jest
.mocked(config)
.getEnv.mockImplementation((value) => (value === 'ai.openAIApiKey' ? '' : 'openai'));
const aiService = new AIService();
await expect(async () => await aiService.prompt([])).rejects.toThrow(ApplicationError);
});
test('should not throw if prompting with known provider type', () => {
jest.mocked(config).getEnv.mockReturnValue('openai');
const aiService = new AIService();
expect(async () => await aiService.prompt([])).not.toThrow(ApplicationError);
});
});
describe('prompt', () => {
test('should call model.prompt', async () => {
const service = new AIService();
await service.prompt(['message']);
expect(service.model.prompt).toHaveBeenCalledWith(['message']);
});
});
describe('debugError', () => {
test('should call prompt with error and nodeType', async () => {
const service = new AIService();
const promptSpy = jest.spyOn(service, 'prompt').mockResolvedValue('prompt');
const nodeType = {
description: {
displayName: 'Node Type',
name: 'nodeType',
properties: [],
},
} as unknown as INodeType;
const error = new NodeOperationError(
{
type: 'n8n-nodes-base.error',
typeVersion: 1,
} as INode,
'Error',
);
await service.debugError(error, nodeType);
expect(promptSpy).toHaveBeenCalledWith(createDebugErrorPrompt(error, nodeType));
});
});
});

View File

@@ -0,0 +1,193 @@
import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow';
import {
summarizeNodeTypeProperties,
summarizeOption,
summarizeProperty,
} from '@/services/ai/utils/summarizeNodeTypeProperties';
describe('summarizeOption', () => {
it('should return summarized option with value', () => {
const option: INodePropertyOptions = {
name: 'testOption',
value: 'testValue',
};
const result = summarizeOption(option);
expect(result).toEqual({
name: 'testOption',
value: 'testValue',
});
});
it('should return summarized option with values', () => {
const option: INodePropertyCollection = {
name: 'testOption',
displayName: 'testDisplayName',
values: [
{
name: 'testName',
default: '',
displayName: 'testDisplayName',
type: 'string',
},
],
};
const result = summarizeOption(option);
expect(result).toEqual({
name: 'testOption',
values: [
{
name: 'testDisplayName',
type: 'string',
},
],
});
});
it('should return summarized property', () => {
const option: INodeProperties = {
name: 'testName',
default: '',
displayName: 'testDisplayName',
type: 'string',
};
const result = summarizeOption(option);
expect(result).toEqual({
name: 'testDisplayName',
type: 'string',
});
});
});
describe('summarizeProperty', () => {
it('should return summarized property with displayOptions', () => {
const property: INodeProperties = {
default: '',
name: 'testName',
displayName: 'testDisplayName',
type: 'string',
displayOptions: {
show: {
testOption: ['testValue'],
},
},
};
const result = summarizeProperty(property);
expect(result).toEqual({
name: 'testDisplayName',
type: 'string',
displayOptions: {
show: {
testOption: ['testValue'],
},
},
});
});
it('should return summarized property with options', () => {
const property: INodeProperties = {
name: 'testName',
displayName: 'testDisplayName',
default: '',
type: 'string',
options: [
{
name: 'testOption',
value: 'testValue',
},
],
};
const result = summarizeProperty(property);
expect(result).toEqual({
name: 'testDisplayName',
type: 'string',
options: [
{
name: 'testOption',
value: 'testValue',
},
],
});
});
it('should return summarized property without displayOptions and options', () => {
const property: INodeProperties = {
name: 'testName',
default: '',
displayName: 'testDisplayName',
type: 'string',
};
const result = summarizeProperty(property);
expect(result).toEqual({
name: 'testDisplayName',
type: 'string',
});
});
});
describe('summarizeNodeTypeProperties', () => {
it('should return summarized properties', () => {
const properties: INodeProperties[] = [
{
name: 'testName1',
default: '',
displayName: 'testDisplayName1',
type: 'string',
options: [
{
name: 'testOption1',
value: 'testValue1',
},
],
},
{
name: 'testName2',
default: '',
displayName: 'testDisplayName2',
type: 'number',
options: [
{
name: 'testOption2',
value: 'testValue2',
},
],
},
];
const result = summarizeNodeTypeProperties(properties);
expect(result).toEqual([
{
name: 'testDisplayName1',
type: 'string',
options: [
{
name: 'testOption1',
value: 'testValue1',
},
],
},
{
name: 'testDisplayName2',
type: 'number',
options: [
{
name: 'testOption2',
value: 'testValue2',
},
],
},
]);
});
});