refactor: Use Ask AI feature through AI services instead of hooks (#11027)

This commit is contained in:
Ricardo Espinoza 2024-10-09 11:24:33 -04:00 committed by GitHub
parent e94cda3837
commit 06f51dc7d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 3297 additions and 883 deletions

View file

@ -78,7 +78,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should start chat session from node error view', () => { it('should start chat session from node error view', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json', fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -96,7 +96,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should render chat input correctly', () => { it('should render chat input correctly', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json', fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -129,7 +129,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should render and handle quick replies', () => { it('should render and handle quick replies', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/quick_reply_message_response.json', fixture: 'aiAssistant/quick_reply_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -146,7 +146,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should show quick replies when node is executed after new suggestion', () => { it('should show quick replies when node is executed after new suggestion', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => { cy.intercept('POST', '/rest/ai/chat', (req) => {
req.reply((res) => { req.reply((res) => {
if (['init-error-helper', 'message'].includes(req.body.payload.type)) { if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' }); res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
@ -177,7 +177,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should warn before starting a new session', () => { it('should warn before starting a new session', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json', fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -204,11 +204,11 @@ describe('AI Assistant::enabled', () => {
}); });
it('should apply code diff to code node', () => { it('should apply code diff to code node', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/code_diff_suggestion_response.json', fixture: 'aiAssistant/code_diff_suggestion_response.json',
}).as('chatRequest'); }).as('chatRequest');
cy.intercept('POST', '/rest/ai-assistant/chat/apply-suggestion', { cy.intercept('POST', '/rest/ai/chat/apply-suggestion', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/apply_code_diff_response.json', fixture: 'aiAssistant/apply_code_diff_response.json',
}).as('applySuggestion'); }).as('applySuggestion');
@ -254,7 +254,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should end chat session when `end_session` event is received', () => { it('should end chat session when `end_session` event is received', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/end_session_response.json', fixture: 'aiAssistant/end_session_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -268,7 +268,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('should reset session after it ended and sidebar is closed', () => { it('should reset session after it ended and sidebar is closed', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => { cy.intercept('POST', '/rest/ai/chat', (req) => {
req.reply((res) => { req.reply((res) => {
if (['init-support-chat'].includes(req.body.payload.type)) { if (['init-support-chat'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' }); res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
@ -296,7 +296,7 @@ describe('AI Assistant::enabled', () => {
}); });
it('Should not reset assistant session when workflow is saved', () => { it('Should not reset assistant session when workflow is saved', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json', fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -321,7 +321,7 @@ describe('AI Assistant Credential Help', () => {
}); });
it('should start credential help from node credential', () => { it('should start credential help from node credential', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json', fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -347,7 +347,7 @@ describe('AI Assistant Credential Help', () => {
}); });
it('should start credential help from credential list', () => { it('should start credential help from credential list', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json', fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest'); }).as('chatRequest');
@ -446,7 +446,7 @@ describe('General help', () => {
}); });
it('assistant returns code snippet', () => { it('assistant returns code snippet', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200, statusCode: 200,
fixture: 'aiAssistant/code_snippet_response.json', fixture: 'aiAssistant/code_snippet_response.json',
}).as('chatRequest'); }).as('chatRequest');

View file

@ -91,28 +91,12 @@ return []
}); });
describe('Ask AI', () => { describe('Ask AI', () => {
it('tab should display based on experiment', () => {
WorkflowPage.actions.visit();
cy.window().then((win) => {
win.featureFlags.override('011_ask_AI', 'control');
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');
cy.getByTestId('code-node-tab-ai').should('not.exist');
ndv.actions.close();
win.featureFlags.override('011_ask_AI', undefined);
WorkflowPage.actions.openNode('Code');
cy.getByTestId('code-node-tab-ai').should('not.exist');
});
});
describe('Enabled', () => { describe('Enabled', () => {
beforeEach(() => { beforeEach(() => {
cy.enableFeature('askAi');
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
cy.window().then((win) => {
win.featureFlags.override('011_ask_AI', 'gpt3'); cy.window().then(() => {
WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code', true, true); WorkflowPage.actions.addNodeToCanvas('Code', true, true);
}); });
@ -157,7 +141,7 @@ return []
cy.getByTestId('ask-ai-prompt-input').type(prompt); cy.getByTestId('ask-ai-prompt-input').type(prompt);
cy.intercept('POST', '/rest/ask-ai', { cy.intercept('POST', '/rest/ai/ask-ai', {
statusCode: 200, statusCode: 200,
body: { body: {
data: { data: {
@ -169,9 +153,7 @@ return []
cy.getByTestId('ask-ai-cta').click(); cy.getByTestId('ask-ai-cta').click();
const askAiReq = cy.wait('@ask-ai'); const askAiReq = cy.wait('@ask-ai');
askAiReq askAiReq.its('request.body').should('have.keys', ['question', 'context', 'forNode']);
.its('request.body')
.should('have.keys', ['question', 'model', 'context', 'n8nVersion']);
askAiReq.its('context').should('have.keys', ['schema', 'ndvPushRef', 'pushRef']); askAiReq.its('context').should('have.keys', ['schema', 'ndvPushRef', 'pushRef']);
@ -195,7 +177,7 @@ return []
]; ];
handledCodes.forEach(({ code, message }) => { handledCodes.forEach(({ code, message }) => {
cy.intercept('POST', '/rest/ask-ai', { cy.intercept('POST', '/rest/ai/ask-ai', {
statusCode: code, statusCode: code,
status: code, status: code,
}).as('ask-ai'); }).as('ask-ai');

View file

@ -107,6 +107,9 @@ export interface FrontendSettings {
aiAssistant: { aiAssistant: {
enabled: boolean; enabled: boolean;
}; };
askAi: {
enabled: boolean;
};
deployment: { deployment: {
type: string; type: string;
}; };
@ -154,9 +157,6 @@ export interface FrontendSettings {
banners: { banners: {
dismissed: string[]; dismissed: string[];
}; };
ai: {
enabled: boolean;
};
workflowHistory: { workflowHistory: {
pruneTime: number; pruneTime: number;
licensePruneTime: number; licensePruneTime: number;

View file

@ -93,7 +93,7 @@
"@n8n/permissions": "workspace:*", "@n8n/permissions": "workspace:*",
"@n8n/task-runner": "workspace:*", "@n8n/task-runner": "workspace:*",
"@n8n/typeorm": "0.3.20-12", "@n8n/typeorm": "0.3.20-12",
"@n8n_io/ai-assistant-sdk": "1.9.4", "@n8n_io/ai-assistant-sdk": "1.10.3",
"@n8n_io/license-sdk": "2.13.1", "@n8n_io/license-sdk": "2.13.1",
"@oclif/core": "4.0.7", "@oclif/core": "4.0.7",
"@rudderstack/rudder-sdk-node": "2.0.9", "@rudderstack/rudder-sdk-node": "2.0.9",

View file

@ -91,6 +91,7 @@ export const LICENSE_FEATURES = {
PROJECT_ROLE_EDITOR: 'feat:projectRole:editor', PROJECT_ROLE_EDITOR: 'feat:projectRole:editor',
PROJECT_ROLE_VIEWER: 'feat:projectRole:viewer', PROJECT_ROLE_VIEWER: 'feat:projectRole:viewer',
AI_ASSISTANT: 'feat:aiAssistant', AI_ASSISTANT: 'feat:aiAssistant',
ASK_AI: 'feat:askAi',
COMMUNITY_NODES_CUSTOM_REGISTRY: 'feat:communityNodes:customRegistry', COMMUNITY_NODES_CUSTOM_REGISTRY: 'feat:communityNodes:customRegistry',
} as const; } as const;

View file

@ -7,18 +7,18 @@ import { WritableStream } from 'node:stream/web';
import { Post, RestController } from '@/decorators'; import { Post, RestController } from '@/decorators';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { AiAssistantRequest } from '@/requests'; import { AiAssistantRequest } from '@/requests';
import { AiAssistantService } from '@/services/ai-assistant.service'; import { AiService } from '@/services/ai.service';
type FlushableResponse = Response & { flush: () => void }; type FlushableResponse = Response & { flush: () => void };
@RestController('/ai-assistant') @RestController('/ai')
export class AiAssistantController { export class AiController {
constructor(private readonly aiAssistantService: AiAssistantService) {} constructor(private readonly aiService: AiService) {}
@Post('/chat', { rateLimit: { limit: 100 } }) @Post('/chat', { rateLimit: { limit: 100 } })
async chat(req: AiAssistantRequest.Chat, res: FlushableResponse) { async chat(req: AiAssistantRequest.Chat, res: FlushableResponse) {
try { try {
const aiResponse = await this.aiAssistantService.chat(req.body, req.user); const aiResponse = await this.aiService.chat(req.body, req.user);
if (aiResponse.body) { if (aiResponse.body) {
res.header('Content-type', 'application/json-lines').flush(); res.header('Content-type', 'application/json-lines').flush();
await aiResponse.body.pipeTo( await aiResponse.body.pipeTo(
@ -40,10 +40,21 @@ export class AiAssistantController {
@Post('/chat/apply-suggestion') @Post('/chat/apply-suggestion')
async applySuggestion( async applySuggestion(
req: AiAssistantRequest.ApplySuggestion, req: AiAssistantRequest.ApplySuggestionPayload,
): Promise<AiAssistantSDK.ApplySuggestionResponse> { ): Promise<AiAssistantSDK.ApplySuggestionResponse> {
try { try {
return await this.aiAssistantService.applySuggestion(req.body, req.user); return await this.aiService.applySuggestion(req.body, req.user);
} catch (e) {
assert(e instanceof Error);
ErrorReporterProxy.error(e);
throw new InternalServerError(`Something went wrong: ${e.message}`);
}
}
@Post('/ask-ai')
async askAi(req: AiAssistantRequest.AskAiPayload): Promise<AiAssistantSDK.AskAiResponsePayload> {
try {
return await this.aiService.askAi(req.body, req.user);
} catch (e) { } catch (e) {
assert(e instanceof Error); assert(e instanceof Error);
ErrorReporterProxy.error(e); ErrorReporterProxy.error(e);

View file

@ -92,6 +92,7 @@ export class E2EController {
[LICENSE_FEATURES.PROJECT_ROLE_VIEWER]: false, [LICENSE_FEATURES.PROJECT_ROLE_VIEWER]: false,
[LICENSE_FEATURES.AI_ASSISTANT]: false, [LICENSE_FEATURES.AI_ASSISTANT]: false,
[LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY]: false, [LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY]: false,
[LICENSE_FEATURES.ASK_AI]: false,
}; };
private numericFeatures: Record<NumericLicenseFeature, number> = { private numericFeatures: Record<NumericLicenseFeature, number> = {

View file

@ -255,6 +255,10 @@ export class License {
return this.isFeatureEnabled(LICENSE_FEATURES.AI_ASSISTANT); return this.isFeatureEnabled(LICENSE_FEATURES.AI_ASSISTANT);
} }
isAskAiEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.ASK_AI);
}
isAdvancedExecutionFiltersEnabled() { isAdvancedExecutionFiltersEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS); return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS);
} }

View file

@ -586,5 +586,6 @@ export declare namespace AiAssistantRequest {
type Chat = AuthenticatedRequest<{}, {}, AiAssistantSDK.ChatRequestPayload>; type Chat = AuthenticatedRequest<{}, {}, AiAssistantSDK.ChatRequestPayload>;
type SuggestionPayload = { sessionId: string; suggestionId: string }; type SuggestionPayload = { sessionId: string; suggestionId: string };
type ApplySuggestion = AuthenticatedRequest<{}, {}, SuggestionPayload>; type ApplySuggestionPayload = AuthenticatedRequest<{}, {}, SuggestionPayload>;
type AskAiPayload = AuthenticatedRequest<{}, {}, AiAssistantSDK.AskAiRequestPayload>;
} }

View file

@ -39,7 +39,7 @@ import '@/controllers/annotation-tags.controller.ee';
import '@/controllers/auth.controller'; import '@/controllers/auth.controller';
import '@/controllers/binary-data.controller'; import '@/controllers/binary-data.controller';
import '@/controllers/curl.controller'; import '@/controllers/curl.controller';
import '@/controllers/ai-assistant.controller'; import '@/controllers/ai.controller';
import '@/controllers/dynamic-node-parameters.controller'; import '@/controllers/dynamic-node-parameters.controller';
import '@/controllers/invitation.controller'; import '@/controllers/invitation.controller';
import '@/controllers/me.controller'; import '@/controllers/me.controller';

View file

@ -3,7 +3,6 @@ import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk'; import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk';
import { assert, type IUser } from 'n8n-workflow'; import { assert, type IUser } from 'n8n-workflow';
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { Response } from 'undici';
import config from '@/config'; import config from '@/config';
import type { AiAssistantRequest } from '@/requests'; import type { AiAssistantRequest } from '@/requests';
@ -12,7 +11,7 @@ import { N8N_VERSION } from '../constants';
import { License } from '../license'; import { License } from '../license';
@Service() @Service()
export class AiAssistantService { export class AiService {
private client: AiAssistantClient | undefined; private client: AiAssistantClient | undefined;
constructor( constructor(
@ -40,7 +39,7 @@ export class AiAssistantService {
}); });
} }
async chat(payload: AiAssistantSDK.ChatRequestPayload, user: IUser): Promise<Response> { async chat(payload: AiAssistantSDK.ChatRequestPayload, user: IUser) {
if (!this.client) { if (!this.client) {
await this.init(); await this.init();
} }
@ -57,4 +56,13 @@ export class AiAssistantService {
return await this.client.applySuggestion(payload, { id: user.id }); return await this.client.applySuggestion(payload, { id: user.id });
} }
async askAi(payload: AiAssistantSDK.AskAiRequestPayload, user: IUser) {
if (!this.client) {
await this.init();
}
assert(this.client, 'Assistant client not setup');
return await this.client.askAi(payload, { id: user.id });
}
} }

View file

@ -212,8 +212,8 @@ export class FrontendService {
banners: { banners: {
dismissed: [], dismissed: [],
}, },
ai: { askAi: {
enabled: config.getEnv('ai.enabled'), enabled: false,
}, },
workflowHistory: { workflowHistory: {
pruneTime: -1, pruneTime: -1,
@ -274,6 +274,7 @@ export class FrontendService {
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3'); const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = this.license.isBinaryDataS3Licensed(); const isS3Licensed = this.license.isBinaryDataS3Licensed();
const isAiAssistantEnabled = this.license.isAiAssistantEnabled(); const isAiAssistantEnabled = this.license.isAiAssistantEnabled();
const isAskAiEnabled = this.license.isAskAiEnabled();
this.settings.license.planName = this.license.getPlanName(); this.settings.license.planName = this.license.getPlanName();
this.settings.license.consumerId = this.license.getConsumerId(); this.settings.license.consumerId = this.license.getConsumerId();
@ -330,6 +331,10 @@ export class FrontendService {
this.settings.aiAssistant.enabled = isAiAssistantEnabled; this.settings.aiAssistant.enabled = isAiAssistantEnabled;
} }
if (isAskAiEnabled) {
this.settings.askAi.enabled = isAskAiEnabled;
}
this.settings.mfa.enabled = config.get('mfa.enabled'); this.settings.mfa.enabled = config.get('mfa.enabled');
this.settings.executionMode = config.getEnv('executions.mode'); this.settings.executionMode = config.getEnv('executions.mode');

View file

@ -111,7 +111,7 @@ export const defaultSettings: FrontendSettings = {
mfa: { mfa: {
enabled: false, enabled: false,
}, },
ai: { askAi: {
enabled: false, enabled: false,
}, },
workflowHistory: { workflowHistory: {

View file

@ -1,30 +1,44 @@
import type { IRestApiContext, Schema } from '@/Interface'; import type { IRestApiContext } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils'; import type { AskAiRequest, ChatRequest, ReplaceCodeRequest } from '@/types/assistant.types';
import { makeRestApiRequest, streamRequest } from '@/utils/apiUtils';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
export function chatWithAssistant(
ctx: IRestApiContext,
payload: ChatRequest.RequestPayload,
onMessageUpdated: (data: ChatRequest.ResponsePayload) => void,
onDone: () => void,
onError: (e: Error) => void,
): void {
void streamRequest<ChatRequest.ResponsePayload>(
ctx,
'/ai/chat',
payload,
onMessageUpdated,
onDone,
onError,
);
}
export async function replaceCode(
context: IRestApiContext,
data: ReplaceCodeRequest.RequestPayload,
): Promise<ReplaceCodeRequest.ResponsePayload> {
return await makeRestApiRequest<ReplaceCodeRequest.ResponsePayload>(
context,
'POST',
'/ai/chat/apply-suggestion',
data,
);
}
export async function generateCodeForPrompt( export async function generateCodeForPrompt(
ctx: IRestApiContext, ctx: IRestApiContext,
{ { question, context, forNode }: AskAiRequest.RequestPayload,
question,
context,
model,
n8nVersion,
}: {
question: string;
context: {
schema: Array<{ nodeName: string; schema: Schema }>;
inputSchema: { nodeName: string; schema: Schema };
pushRef: string;
ndvPushRef: string;
};
model: string;
n8nVersion: string;
},
): Promise<{ code: string }> { ): Promise<{ code: string }> {
return await makeRestApiRequest(ctx, 'POST', '/ask-ai', { return await makeRestApiRequest(ctx, 'POST', '/ai/ask-ai', {
question, question,
context, context,
model, forNode,
n8nVersion,
} as IDataObject); } as IDataObject);
} }

View file

@ -1,32 +0,0 @@
import type { IRestApiContext } from '@/Interface';
import type { ChatRequest, ReplaceCodeRequest } from '@/types/assistant.types';
import { makeRestApiRequest, streamRequest } from '@/utils/apiUtils';
export function chatWithAssistant(
ctx: IRestApiContext,
payload: ChatRequest.RequestPayload,
onMessageUpdated: (data: ChatRequest.ResponsePayload) => void,
onDone: () => void,
onError: (e: Error) => void,
): void {
void streamRequest<ChatRequest.ResponsePayload>(
ctx,
'/ai-assistant/chat',
payload,
onMessageUpdated,
onDone,
onError,
);
}
export async function replaceCode(
context: IRestApiContext,
data: ReplaceCodeRequest.RequestPayload,
): Promise<ReplaceCodeRequest.ResponsePayload> {
return await makeRestApiRequest<ReplaceCodeRequest.ResponsePayload>(
context,
'POST',
'/ai-assistant/chat/apply-suggestion',
data,
);
}

View file

@ -7,8 +7,6 @@ import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { getSchemas, getParentNodes } from './utils'; import { getSchemas, getParentNodes } from './utils';
import { ASK_AI_EXPERIMENT } from '@/constants';
import { usePostHog } from '@/stores/posthog.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { generateCodeForPrompt } from '@/api/ai'; import { generateCodeForPrompt } from '@/api/ai';
@ -16,6 +14,8 @@ import { generateCodeForPrompt } from '@/api/ai';
import { format } from 'prettier'; import { format } from 'prettier';
import jsParser from 'prettier/plugins/babel'; import jsParser from 'prettier/plugins/babel';
import * as estree from 'prettier/plugins/estree'; import * as estree from 'prettier/plugins/estree';
import { useSettingsStore } from '@/stores/settings.store';
import type { AskAiRequest } from '@/types/assistant.types';
const emit = defineEmits<{ const emit = defineEmits<{
valueChanged: [value: IUpdateInformation]; valueChanged: [value: IUpdateInformation];
@ -27,8 +27,8 @@ const props = defineProps<{
path: string; path: string;
}>(); }>();
const posthog = usePostHog();
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const i18n = useI18n(); const i18n = useI18n();
@ -94,13 +94,8 @@ async function onSubmit() {
try { try {
const schemas = getSchemas(); const schemas = getSchemas();
const version = rootStore.versionCli;
const model =
usePostHog().getVariant(ASK_AI_EXPERIMENT.name) === ASK_AI_EXPERIMENT.gpt4
? 'gpt-4'
: 'gpt-3.5-turbo-16k';
const payload = { const payload: AskAiRequest.RequestPayload = {
question: prompt.value, question: prompt.value,
context: { context: {
schema: schemas.parentNodesSchemas, schema: schemas.parentNodesSchemas,
@ -108,13 +103,12 @@ async function onSubmit() {
ndvPushRef: useNDVStore().pushRef, ndvPushRef: useNDVStore().pushRef,
pushRef: rootStore.pushRef, pushRef: rootStore.pushRef,
}, },
model, forNode: 'transform',
n8nVersion: version,
}; };
switch (type) { switch (type) {
case 'askAiCodeGeneration': case 'askAiCodeGeneration':
let value; let value;
if (posthog.isAiEnabled()) { if (settingsStore.isAskAiEnabled) {
const { restApiContext } = useRootStore(); const { restApiContext } = useRootStore();
const { code } = await generateCodeForPrompt(restApiContext, payload); const { code } = await generateCodeForPrompt(restApiContext, payload);
value = code; value = code;

View file

@ -16,16 +16,15 @@ import { useI18n } from '@/composables/useI18n';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { usePostHog } from '@/stores/posthog.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { executionDataToJson } from '@/utils/nodeTypesUtils'; import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { import {
ASK_AI_EXPERIMENT,
ASK_AI_MAX_PROMPT_LENGTH, ASK_AI_MAX_PROMPT_LENGTH,
ASK_AI_MIN_PROMPT_LENGTH, ASK_AI_MIN_PROMPT_LENGTH,
ASK_AI_LOADING_DURATION_MS, ASK_AI_LOADING_DURATION_MS,
} from '@/constants'; } from '@/constants';
import type { AskAiRequest } from '@/types/assistant.types';
const emit = defineEmits<{ const emit = defineEmits<{
submit: [code: string]; submit: [code: string];
@ -89,7 +88,7 @@ function getParentNodes() {
return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i; return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i;
}) })
.map((n) => getNodeByName(n.name)) .map((n) => getNodeByName(n.name))
.filter((n) => n !== null) as INodeUi[]; .filter((n) => n !== null);
} }
function getSchemas() { function getSchemas() {
@ -156,24 +155,19 @@ async function onSubmit() {
const rootStore = useRootStore(); const rootStore = useRootStore();
try { const payload: AskAiRequest.RequestPayload = {
const version = rootStore.versionCli; question: prompt.value,
const model = context: {
usePostHog().getVariant(ASK_AI_EXPERIMENT.name) === ASK_AI_EXPERIMENT.gpt4 schema: schemas.parentNodesSchemas,
? 'gpt-4' inputSchema: schemas.inputSchema!,
: 'gpt-3.5-turbo-16k'; ndvPushRef: useNDVStore().pushRef,
pushRef: rootStore.pushRef,
},
forNode: 'code',
};
const { code } = await generateCodeForPrompt(restApiContext, { try {
question: prompt.value, const { code } = await generateCodeForPrompt(restApiContext, payload);
context: {
schema: schemas.parentNodesSchemas,
inputSchema: schemas.inputSchema!,
ndvPushRef: useNDVStore().pushRef,
pushRef: rootStore.pushRef,
},
model,
n8nVersion: version,
});
stopLoading(); stopLoading();
emit('replaceCode', code); emit('replaceCode', code);

View file

@ -15,7 +15,6 @@ import { type Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, w
import { CODE_NODE_TYPE } from '@/constants'; import { CODE_NODE_TYPE } from '@/constants';
import { codeNodeEditorEventBus } from '@/event-bus'; import { codeNodeEditorEventBus } from '@/event-bus';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { usePostHog } from '@/stores/posthog.store';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import AskAI from './AskAI/AskAI.vue'; import AskAI from './AskAI/AskAI.vue';
@ -27,6 +26,7 @@ import { codeNodeEditorTheme } from './theme';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { dropInCodeEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop'; import { dropInCodeEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
import { useSettingsStore } from '@/stores/settings.store';
type Props = { type Props = {
mode: CodeExecutionMode; mode: CodeExecutionMode;
@ -67,9 +67,9 @@ const { autocompletionExtension } = useCompleter(() => props.mode, editor);
const { createLinter } = useLinter(() => props.mode, editor); const { createLinter } = useLinter(() => props.mode, editor);
const rootStore = useRootStore(); const rootStore = useRootStore();
const posthog = usePostHog();
const i18n = useI18n(); const i18n = useI18n();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const settingsStore = useSettingsStore();
onMounted(() => { onMounted(() => {
if (!props.isReadOnly) codeNodeEditorEventBus.on('highlightLine', highlightLine); if (!props.isReadOnly) codeNodeEditorEventBus.on('highlightLine', highlightLine);
@ -146,8 +146,8 @@ onBeforeUnmount(() => {
if (!props.isReadOnly) codeNodeEditorEventBus.off('highlightLine', highlightLine); if (!props.isReadOnly) codeNodeEditorEventBus.off('highlightLine', highlightLine);
}); });
const aiEnabled = computed(() => { const askAiEnabled = computed(() => {
return posthog.isAiEnabled() && props.language === 'javaScript'; return settingsStore.isAskAiEnabled && props.language === 'javaScript';
}); });
const placeholder = computed(() => { const placeholder = computed(() => {
@ -218,7 +218,7 @@ watch(
}, },
); );
watch( watch(
aiEnabled, askAiEnabled,
async (isEnabled) => { async (isEnabled) => {
if (isEnabled && !props.modelValue) { if (isEnabled && !props.modelValue) {
emit('update:modelValue', placeholder.value); emit('update:modelValue', placeholder.value);
@ -391,7 +391,7 @@ async function onDrop(value: string, event: MouseEvent) {
@mouseout="onMouseOut" @mouseout="onMouseOut"
> >
<el-tabs <el-tabs
v-if="aiEnabled" v-if="askAiEnabled"
ref="tabs" ref="tabs"
v-model="activeTab" v-model="activeTab"
type="card" type="card"

View file

@ -43,7 +43,7 @@ import {
type Themed, type Themed,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { useCanvasStore } from '@/stores/canvas.store'; import { useCanvasStore } from '@/stores/canvas.store';
import { usePostHog } from '../../../../stores/posthog.store'; import { useSettingsStore } from '@/stores/settings.store';
interface ViewStack { interface ViewStack {
uuid?: string; uuid?: string;
@ -79,6 +79,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
const nodeCreatorStore = useNodeCreatorStore(); const nodeCreatorStore = useNodeCreatorStore();
const { getActiveItemIndex } = useKeyboardNavigation(); const { getActiveItemIndex } = useKeyboardNavigation();
const i18n = useI18n(); const i18n = useI18n();
const settingsStore = useSettingsStore();
const viewStacks = ref<ViewStack[]>([]); const viewStacks = ref<ViewStack[]>([]);
@ -360,8 +361,8 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
const subcategory = stack?.subcategory ?? DEFAULT_SUBCATEGORY; const subcategory = stack?.subcategory ?? DEFAULT_SUBCATEGORY;
let itemsInSubcategory = itemsBySubcategory.value[subcategory]; let itemsInSubcategory = itemsBySubcategory.value[subcategory];
const aiEnabled = usePostHog().isAiEnabled(); const isAskAiEnabled = settingsStore.isAskAiEnabled;
if (!aiEnabled) { if (!isAskAiEnabled) {
itemsInSubcategory = itemsInSubcategory.filter( itemsInSubcategory = itemsInSubcategory.filter(
(item) => item.key !== AI_TRANSFORM_NODE_TYPE, (item) => item.key !== AI_TRANSFORM_NODE_TYPE,
); );

View file

@ -21,7 +21,7 @@ import { i18n } from '@/plugins/i18n';
import { sortBy } from 'lodash-es'; import { sortBy } from 'lodash-es';
import * as changeCase from 'change-case'; import * as changeCase from 'change-case';
import { usePostHog } from '@/stores/posthog.store'; import { useSettingsStore } from '@/stores/settings.store';
export function transformNodeType( export function transformNodeType(
node: SimplifiedNodeType, node: SimplifiedNodeType,
@ -78,8 +78,8 @@ export function sortNodeCreateElements(nodes: INodeCreateElement[]) {
} }
export function searchNodes(searchFilter: string, items: INodeCreateElement[]) { export function searchNodes(searchFilter: string, items: INodeCreateElement[]) {
const aiEnabled = usePostHog().isAiEnabled(); const askAiEnabled = useSettingsStore().isAskAiEnabled;
if (!aiEnabled) { if (!askAiEnabled) {
items = items.filter((item) => item.key !== AI_TRANSFORM_NODE_TYPE); items = items.filter((item) => item.key !== AI_TRANSFORM_NODE_TYPE);
} }

View file

@ -668,13 +668,6 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
export const MAIN_AUTH_FIELD_NAME = 'authentication'; export const MAIN_AUTH_FIELD_NAME = 'authentication';
export const NODE_RESOURCE_FIELD_NAME = 'resource'; export const NODE_RESOURCE_FIELD_NAME = 'resource';
export const ASK_AI_EXPERIMENT = {
name: '011_ask_AI',
control: 'control',
gpt3: 'gpt3',
gpt4: 'gpt4',
};
export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '017_template_credential_setup_v2'; export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '017_template_credential_setup_v2';
export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = { export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = {
@ -701,7 +694,6 @@ export const CREDENTIAL_DOCS_EXPERIMENT = {
variant: 'variant', variant: 'variant',
}; };
export const EXPERIMENTS_TO_TRACK = [ export const EXPERIMENTS_TO_TRACK = [
ASK_AI_EXPERIMENT.name,
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name, CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
AI_ASSISTANT_EXPERIMENT.name, AI_ASSISTANT_EXPERIMENT.name,

View file

@ -15,7 +15,7 @@ import { merge } from 'lodash-es';
import { DEFAULT_POSTHOG_SETTINGS } from './posthog.test'; import { DEFAULT_POSTHOG_SETTINGS } from './posthog.test';
import { AI_ASSISTANT_EXPERIMENT, VIEWS } from '@/constants'; import { AI_ASSISTANT_EXPERIMENT, VIEWS } from '@/constants';
import { reactive } from 'vue'; import { reactive } from 'vue';
import * as chatAPI from '@/api/assistant'; import * as chatAPI from '@/api/ai';
import * as telemetryModule from '@/composables/useTelemetry'; import * as telemetryModule from '@/composables/useTelemetry';
import type { Telemetry } from '@/plugins/telemetry'; import type { Telemetry } from '@/plugins/telemetry';

View file

@ -1,4 +1,4 @@
import { chatWithAssistant, replaceCode } from '@/api/assistant'; import { chatWithAssistant, replaceCode } from '@/api/ai';
import { import {
VIEWS, VIEWS,
EDITABLE_CANVAS_VIEWS, EDITABLE_CANVAS_VIEWS,

View file

@ -6,11 +6,7 @@ import { useUsersStore } from '@/stores/users.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import type { FeatureFlags, IDataObject } from 'n8n-workflow'; import type { FeatureFlags, IDataObject } from 'n8n-workflow';
import { import { EXPERIMENTS_TO_TRACK, LOCAL_STORAGE_EXPERIMENT_OVERRIDES } from '@/constants';
ASK_AI_EXPERIMENT,
EXPERIMENTS_TO_TRACK,
LOCAL_STORAGE_EXPERIMENT_OVERRIDES,
} from '@/constants';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
@ -42,14 +38,6 @@ export const usePostHog = defineStore('posthog', () => {
return overrides.value[experiment] ?? featureFlags.value?.[experiment]; return overrides.value[experiment] ?? featureFlags.value?.[experiment];
}; };
const isAiEnabled = () => {
const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes(
(getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string,
);
return isAiExperimentEnabled && settingsStore.settings.ai.enabled;
};
const isVariantEnabled = (experiment: string, variant: string) => { const isVariantEnabled = (experiment: string, variant: string) => {
return getVariant(experiment) === variant; return getVariant(experiment) === variant;
}; };
@ -195,7 +183,6 @@ export const usePostHog = defineStore('posthog', () => {
return { return {
init, init,
isAiEnabled,
isFeatureEnabled, isFeatureEnabled,
isVariantEnabled, isVariantEnabled,
getVariant, getVariant,

View file

@ -87,6 +87,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const isAiAssistantEnabled = computed(() => settings.value.aiAssistant?.enabled); const isAiAssistantEnabled = computed(() => settings.value.aiAssistant?.enabled);
const isAskAiEnabled = computed(() => settings.value.askAi?.enabled);
const showSetupPage = computed(() => userManagement.value.showSetupOnFirstLoad); const showSetupPage = computed(() => userManagement.value.showSetupOnFirstLoad);
const deploymentType = computed(() => settings.value.deployment?.type || 'default'); const deploymentType = computed(() => settings.value.deployment?.type || 'default');
@ -410,6 +412,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
saveManualExecutions, saveManualExecutions,
saveDataProgressExecution, saveDataProgressExecution,
isCommunityPlan, isCommunityPlan,
isAskAiEnabled,
reset, reset,
testLdapConnection, testLdapConnection,
getLdapConfig, getLdapConfig,

View file

@ -195,3 +195,16 @@ export namespace ReplaceCodeRequest {
parameters: INodeParameters; parameters: INodeParameters;
} }
} }
export namespace AskAiRequest {
export interface RequestPayload {
question: string;
context: {
schema: ChatRequest.NodeExecutionSchema[];
inputSchema: ChatRequest.NodeExecutionSchema;
pushRef: string;
ndvPushRef: string;
};
forNode: 'code' | 'transform';
}
}

File diff suppressed because it is too large Load diff