mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
feat: Add Ask assistant behind feature flag (#9995)
Co-authored-by: Ricardo Espinoza <ricardo@n8n.io> Co-authored-by: Milorad Filipovic <milorad@n8n.io>
This commit is contained in:
@@ -90,6 +90,7 @@
|
||||
"@n8n/n8n-nodes-langchain": "workspace:*",
|
||||
"@n8n/permissions": "workspace:*",
|
||||
"@n8n/typeorm": "0.3.20-10",
|
||||
"@n8n_io/ai-assistant-sdk": "1.9.4",
|
||||
"@n8n_io/license-sdk": "2.13.0",
|
||||
"@oclif/core": "4.0.7",
|
||||
"@rudderstack/rudder-sdk-node": "2.0.7",
|
||||
|
||||
@@ -249,6 +249,10 @@ export class License {
|
||||
return this.isFeatureEnabled(LICENSE_FEATURES.SAML);
|
||||
}
|
||||
|
||||
isAiAssistantEnabled() {
|
||||
return this.isFeatureEnabled(LICENSE_FEATURES.AI_ASSISTANT);
|
||||
}
|
||||
|
||||
isAdvancedExecutionFiltersEnabled() {
|
||||
return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import '@/controllers/activeWorkflows.controller';
|
||||
import '@/controllers/auth.controller';
|
||||
import '@/controllers/binaryData.controller';
|
||||
import '@/controllers/curl.controller';
|
||||
import '@/controllers/aiAssistant.controller';
|
||||
import '@/controllers/dynamicNodeParameters.controller';
|
||||
import '@/controllers/invitation.controller';
|
||||
import '@/controllers/me.controller';
|
||||
|
||||
@@ -569,6 +569,15 @@ export const schema = {
|
||||
},
|
||||
},
|
||||
|
||||
aiAssistant: {
|
||||
baseUrl: {
|
||||
doc: 'Base URL of the AI assistant service',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_AI_ASSISTANT_BASE_URL',
|
||||
},
|
||||
},
|
||||
|
||||
expression: {
|
||||
evaluator: {
|
||||
doc: 'Expression evaluator to use',
|
||||
|
||||
@@ -90,6 +90,7 @@ export const LICENSE_FEATURES = {
|
||||
PROJECT_ROLE_ADMIN: 'feat:projectRole:admin',
|
||||
PROJECT_ROLE_EDITOR: 'feat:projectRole:editor',
|
||||
PROJECT_ROLE_VIEWER: 'feat:projectRole:viewer',
|
||||
AI_ASSISTANT: 'feat:aiAssistant',
|
||||
COMMUNITY_NODES_CUSTOM_REGISTRY: 'feat:communityNodes:customRegistry',
|
||||
} as const;
|
||||
|
||||
|
||||
44
packages/cli/src/controllers/aiAssistant.controller.ts
Normal file
44
packages/cli/src/controllers/aiAssistant.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Post, RestController } from '@/decorators';
|
||||
import { AiAssistantService } from '@/services/aiAsisstant.service';
|
||||
import { AiAssistantRequest } from '@/requests';
|
||||
import { Response } from 'express';
|
||||
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
|
||||
import { Readable, promises } from 'node:stream';
|
||||
import { InternalServerError } from 'express-openapi-validator/dist/openapi.validator';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { ErrorReporterProxy } from 'n8n-workflow';
|
||||
|
||||
@RestController('/ai-assistant')
|
||||
export class AiAssistantController {
|
||||
constructor(private readonly aiAssistantService: AiAssistantService) {}
|
||||
|
||||
@Post('/chat', { rateLimit: { limit: 100 } })
|
||||
async chat(req: AiAssistantRequest.Chat, res: Response) {
|
||||
try {
|
||||
const stream = await this.aiAssistantService.chat(req.body, req.user);
|
||||
|
||||
if (stream.body) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
await promises.pipeline(Readable.fromWeb(stream.body), res);
|
||||
}
|
||||
} catch (e) {
|
||||
// todo add sentry reporting
|
||||
assert(e instanceof Error);
|
||||
ErrorReporterProxy.error(e);
|
||||
throw new InternalServerError({ message: `Something went wrong: ${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/chat/apply-suggestion')
|
||||
async applySuggestion(
|
||||
req: AiAssistantRequest.ApplySuggestion,
|
||||
): Promise<AiAssistantSDK.ApplySuggestionResponse> {
|
||||
try {
|
||||
return await this.aiAssistantService.applySuggestion(req.body, req.user);
|
||||
} catch (e) {
|
||||
assert(e instanceof Error);
|
||||
ErrorReporterProxy.error(e);
|
||||
throw new InternalServerError({ message: `Something went wrong: ${e.message}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ export class E2EController {
|
||||
[LICENSE_FEATURES.PROJECT_ROLE_ADMIN]: false,
|
||||
[LICENSE_FEATURES.PROJECT_ROLE_EDITOR]: false,
|
||||
[LICENSE_FEATURES.PROJECT_ROLE_VIEWER]: false,
|
||||
[LICENSE_FEATURES.AI_ASSISTANT]: false,
|
||||
[LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY]: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { Project, ProjectType } from '@db/entities/Project';
|
||||
import type { ProjectRole } from './databases/entities/ProjectRelation';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { ScopesField } from './services/role.service';
|
||||
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
|
||||
|
||||
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
|
||||
@Expose()
|
||||
@@ -601,3 +602,14 @@ export declare namespace NpsSurveyRequest {
|
||||
// once some schema validation is added
|
||||
type NpsSurveyUpdate = AuthenticatedRequest<{}, {}, unknown>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// /ai-assistant
|
||||
// ----------------------------------
|
||||
|
||||
export declare namespace AiAssistantRequest {
|
||||
type Chat = AuthenticatedRequest<{}, {}, AiAssistantSDK.ChatRequestPayload>;
|
||||
|
||||
type SuggestionPayload = { sessionId: string; suggestionId: string };
|
||||
type ApplySuggestion = AuthenticatedRequest<{}, {}, SuggestionPayload>;
|
||||
}
|
||||
|
||||
54
packages/cli/src/services/aiAsisstant.service.ts
Normal file
54
packages/cli/src/services/aiAsisstant.service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Service } from 'typedi';
|
||||
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
|
||||
import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk';
|
||||
import { assert, type IUser } from 'n8n-workflow';
|
||||
import { License } from '../License';
|
||||
import { N8N_VERSION } from '../constants';
|
||||
import config from '@/config';
|
||||
import type { AiAssistantRequest } from '@/requests';
|
||||
import type { Response } from 'undici';
|
||||
|
||||
@Service()
|
||||
export class AiAssistantService {
|
||||
private client: AiAssistantClient | undefined;
|
||||
|
||||
constructor(private readonly licenseService: License) {}
|
||||
|
||||
async init() {
|
||||
const aiAssistantEnabled = this.licenseService.isAiAssistantEnabled();
|
||||
if (!aiAssistantEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const licenseCert = await this.licenseService.loadCertStr();
|
||||
const consumerId = this.licenseService.getConsumerId();
|
||||
const baseUrl = config.get('aiAssistant.baseUrl');
|
||||
const logLevel = config.getEnv('logs.level');
|
||||
|
||||
this.client = new AiAssistantClient({
|
||||
licenseCert,
|
||||
consumerId,
|
||||
n8nVersion: N8N_VERSION,
|
||||
baseUrl,
|
||||
logLevel,
|
||||
});
|
||||
}
|
||||
|
||||
async chat(payload: AiAssistantSDK.ChatRequestPayload, user: IUser): Promise<Response> {
|
||||
if (!this.client) {
|
||||
await this.init();
|
||||
}
|
||||
assert(this.client, 'Assistant client not setup');
|
||||
|
||||
return await this.client.chat(payload, { id: user.id });
|
||||
}
|
||||
|
||||
async applySuggestion(payload: AiAssistantRequest.SuggestionPayload, user: IUser) {
|
||||
if (!this.client) {
|
||||
await this.init();
|
||||
}
|
||||
assert(this.client, 'Assistant client not setup');
|
||||
|
||||
return await this.client.applySuggestion(payload, { id: user.id });
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,9 @@ export class FrontendService {
|
||||
workflowTagsDisabled: config.getEnv('workflowTagsDisabled'),
|
||||
logLevel: config.getEnv('logs.level'),
|
||||
hiringBannerEnabled: config.getEnv('hiringBanner.enabled'),
|
||||
aiAssistant: {
|
||||
enabled: false,
|
||||
},
|
||||
templates: {
|
||||
enabled: this.globalConfig.templates.enabled,
|
||||
host: this.globalConfig.templates.host,
|
||||
@@ -279,6 +282,7 @@ export class FrontendService {
|
||||
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
|
||||
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
||||
const isS3Licensed = this.license.isBinaryDataS3Licensed();
|
||||
const isAiAssistantEnabled = this.license.isAiAssistantEnabled();
|
||||
|
||||
this.settings.license.planName = this.license.getPlanName();
|
||||
this.settings.license.consumerId = this.license.getConsumerId();
|
||||
@@ -331,6 +335,10 @@ export class FrontendService {
|
||||
this.settings.missingPackages = this.communityPackagesService.hasMissingPackages;
|
||||
}
|
||||
|
||||
if (isAiAssistantEnabled) {
|
||||
this.settings.aiAssistant.enabled = isAiAssistantEnabled;
|
||||
}
|
||||
|
||||
this.settings.mfa.enabled = config.get('mfa.enabled');
|
||||
|
||||
this.settings.executionMode = config.getEnv('executions.mode');
|
||||
|
||||
Reference in New Issue
Block a user