feat: AI Workflow Builder backend (no-changelog) (#14837)

This commit is contained in:
oleg
2025-04-24 08:43:35 +02:00
committed by GitHub
parent 8c4b9f73f1
commit 1b1d6043d6
24 changed files with 1655 additions and 26 deletions

View File

@@ -100,6 +100,7 @@
"@n8n/n8n-nodes-langchain": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/task-runner": "workspace:*",
"@n8n/ai-workflow-builder": "workspace:*",
"@n8n/typeorm": "0.3.20-12",
"@n8n_io/ai-assistant-sdk": "1.13.0",
"@n8n_io/license-sdk": "2.20.0",

View File

@@ -8,14 +8,15 @@ import { mock } from 'jest-mock-extended';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import type { AuthenticatedRequest } from '@/requests';
import type { WorkflowBuilderService } from '@/services/ai-workflow-builder.service';
import type { AiService } from '@/services/ai.service';
import { AiController, type FlushableResponse } from '../ai.controller';
describe('AiController', () => {
const aiService = mock<AiService>();
const controller = new AiController(aiService, mock(), mock());
const workflowBuilderService = mock<WorkflowBuilderService>();
const controller = new AiController(aiService, workflowBuilderService, mock(), mock());
const request = mock<AuthenticatedRequest>({
user: { id: 'user123' },

View File

@@ -4,6 +4,7 @@ import {
AiApplySuggestionRequestDto,
AiAskRequestDto,
AiFreeCreditsRequestDto,
AiBuilderChatRequestDto,
} from '@n8n/api-types';
import { Body, Post, RestController } from '@n8n/decorators';
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
@@ -16,6 +17,7 @@ import { FREE_AI_CREDITS_CREDENTIAL_NAME } from '@/constants';
import { CredentialsService } from '@/credentials/credentials.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { AuthenticatedRequest } from '@/requests';
import { WorkflowBuilderService } from '@/services/ai-workflow-builder.service';
import { AiService } from '@/services/ai.service';
import { UserService } from '@/services/user.service';
@@ -25,10 +27,40 @@ export type FlushableResponse = Response & { flush: () => void };
export class AiController {
constructor(
private readonly aiService: AiService,
private readonly workflowBuilderService: WorkflowBuilderService,
private readonly credentialsService: CredentialsService,
private readonly userService: UserService,
) {}
@Post('/build', { rateLimit: { limit: 100 } })
async build(
req: AuthenticatedRequest,
res: FlushableResponse,
@Body payload: AiBuilderChatRequestDto,
) {
try {
const aiResponse = this.workflowBuilderService.chat(
{
question: payload.payload.question ?? '',
},
req.user,
);
res.header('Content-type', 'application/json-lines').flush();
// Handle the stream
for await (const chunk of aiResponse) {
res.flush();
res.write(JSON.stringify(chunk) + '⧉⇋⇋➽⌑⧉§§\n');
}
res.end();
} catch (e) {
assert(e instanceof Error);
throw new InternalServerError(e.message, e);
}
}
@Post('/chat', { rateLimit: { limit: 100 } })
async chat(req: AuthenticatedRequest, res: FlushableResponse, @Body payload: AiChatRequestDto) {
try {

View File

@@ -0,0 +1,40 @@
import { AiWorkflowBuilderService } from '@n8n/ai-workflow-builder';
import { GlobalConfig } from '@n8n/config';
import { Service } from '@n8n/di';
import type { IUser } from 'n8n-workflow';
import { N8N_VERSION } from '@/constants';
import { License } from '@/license';
import { NodeTypes } from '@/node-types';
/**
* This service wraps the actual AiWorkflowBuilderService to avoid circular dependencies.
* Instead of extending, we're delegating to the real service which is created on-demand.
*/
@Service()
export class WorkflowBuilderService {
private service: AiWorkflowBuilderService | undefined;
constructor(
private readonly nodeTypes: NodeTypes,
private readonly license: License,
private readonly config: GlobalConfig,
) {}
private getService(): AiWorkflowBuilderService {
if (!this.service) {
this.service = new AiWorkflowBuilderService(
this.license,
this.nodeTypes,
this.config,
N8N_VERSION,
);
}
return this.service;
}
async *chat(payload: { question: string }, user: IUser) {
const service = this.getService();
yield* service.chat(payload, user);
}
}