mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat: AI Workflow Builder agent (no-changelog) (#17423)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,47 +1,34 @@
|
||||
import { AiWorkflowBuilderService } from '@n8n/ai-workflow-builder';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import fs from 'fs';
|
||||
import { jsonParse, UserError } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { NodeTypes } from '@/node-types';
|
||||
|
||||
import { WorkerPool } from './worker-pool';
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
interface WorkflowGeneratedMessage {
|
||||
role: 'assistant';
|
||||
type: 'workflow-generated';
|
||||
codeSnippet: string;
|
||||
}
|
||||
// interface WorkflowGenerationDatasetItem {
|
||||
// prompt: string;
|
||||
// referenceWorkflow: string;
|
||||
// }
|
||||
// We'll use this later for evals
|
||||
// async function _waitForWorkflowGenerated(aiResponse: AsyncGenerator<{ messages: any[] }>) {
|
||||
// let workflowJson: string | undefined;
|
||||
|
||||
interface WorkflowGenerationDatasetItem {
|
||||
prompt: string;
|
||||
referenceWorkflow: string;
|
||||
}
|
||||
// for await (const chunk of aiResponse) {
|
||||
// const wfGeneratedMessage = chunk.messages.find(
|
||||
// (m): m is WorkflowGeneratedMessage =>
|
||||
// 'type' in m && (m as { type?: string }).type === 'workflow-generated',
|
||||
// );
|
||||
|
||||
async function waitForWorkflowGenerated(aiResponse: AsyncGenerator<{ messages: any[] }>) {
|
||||
let workflowJson: string | undefined;
|
||||
// if (wfGeneratedMessage?.codeSnippet) {
|
||||
// workflowJson = wfGeneratedMessage.codeSnippet;
|
||||
// }
|
||||
// }
|
||||
|
||||
for await (const chunk of aiResponse) {
|
||||
const wfGeneratedMessage = chunk.messages.find(
|
||||
(m): m is WorkflowGeneratedMessage =>
|
||||
'type' in m && (m as { type?: string }).type === 'workflow-generated',
|
||||
);
|
||||
// if (!workflowJson) {
|
||||
// // FIXME: Use proper error class
|
||||
// throw new UserError('No workflow generated message found in AI response');
|
||||
// }
|
||||
|
||||
if (wfGeneratedMessage?.codeSnippet) {
|
||||
workflowJson = wfGeneratedMessage.codeSnippet;
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflowJson) {
|
||||
// FIXME: Use proper error class
|
||||
throw new UserError('No workflow generated message found in AI response');
|
||||
}
|
||||
|
||||
return workflowJson;
|
||||
}
|
||||
// return workflowJson;
|
||||
// }
|
||||
|
||||
const flagsSchema = z.object({
|
||||
prompt: z
|
||||
@@ -86,135 +73,139 @@ export class TTWFGenerateCommand extends BaseCommand<z.infer<typeof flagsSchema>
|
||||
/**
|
||||
* Reads the dataset file in JSONL format
|
||||
*/
|
||||
private async readDataset(filePath: string): Promise<WorkflowGenerationDatasetItem[]> {
|
||||
try {
|
||||
const data = await fs.promises.readFile(filePath, { encoding: 'utf-8' });
|
||||
// We'll use this later for evals
|
||||
// private async readDataset(filePath: string): Promise<WorkflowGenerationDatasetItem[]> {
|
||||
// try {
|
||||
// const data = await fs.promises.readFile(filePath, { encoding: 'utf-8' });
|
||||
|
||||
const lines = data.split('\n').filter((line) => line.trim() !== '');
|
||||
// const lines = data.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
if (lines.length === 0) {
|
||||
throw new UserError('Dataset file is empty or contains no valid lines');
|
||||
}
|
||||
// if (lines.length === 0) {
|
||||
// throw new UserError('Dataset file is empty or contains no valid lines');
|
||||
// }
|
||||
|
||||
return lines.map((line, index) => {
|
||||
try {
|
||||
return jsonParse<WorkflowGenerationDatasetItem>(line);
|
||||
} catch (error) {
|
||||
throw new UserError(`Invalid JSON line on index: ${index}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw new UserError(`Failed to read dataset file: ${error}`);
|
||||
}
|
||||
}
|
||||
// return lines.map((line, index) => {
|
||||
// try {
|
||||
// return jsonParse<WorkflowGenerationDatasetItem>(line);
|
||||
// } catch (error) {
|
||||
// throw new UserError(`Invalid JSON line on index: ${index}`);
|
||||
// }
|
||||
// });
|
||||
// } catch (error) {
|
||||
// throw new UserError(`Failed to read dataset file: ${error}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
async run() {
|
||||
const { flags } = this;
|
||||
this.logger.error(
|
||||
'This command is displayed until all ai-workflow builder related PR are merged',
|
||||
);
|
||||
// const { flags } = this;
|
||||
|
||||
if (!flags.input && !flags.prompt) {
|
||||
throw new UserError('Either --input or --prompt must be provided.');
|
||||
}
|
||||
// if (!flags.input && !flags.prompt) {
|
||||
// throw new UserError('Either --input or --prompt must be provided.');
|
||||
// }
|
||||
|
||||
if (flags.input && flags.prompt) {
|
||||
throw new UserError('You cannot use --input and --prompt together. Use one or the other.');
|
||||
}
|
||||
// if (flags.input && flags.prompt) {
|
||||
// throw new UserError('You cannot use --input and --prompt together. Use one or the other.');
|
||||
// }
|
||||
|
||||
const nodeTypes = Container.get(NodeTypes);
|
||||
const wfBuilder = new AiWorkflowBuilderService(nodeTypes);
|
||||
// const nodeTypes = Container.get(NodeTypes);
|
||||
// const wfBuilder = new AiWorkflowBuilderService(nodeTypes);
|
||||
|
||||
if (flags.prompt) {
|
||||
// Single prompt mode
|
||||
if (flags.output && fs.existsSync(flags.output)) {
|
||||
if (fs.lstatSync(flags.output).isDirectory()) {
|
||||
this.logger.info('The parameter --output must be a writeable file');
|
||||
return;
|
||||
}
|
||||
// if (flags.prompt) {
|
||||
// // Single prompt mode
|
||||
// if (flags.output && fs.existsSync(flags.output)) {
|
||||
// if (fs.lstatSync(flags.output).isDirectory()) {
|
||||
// this.logger.info('The parameter --output must be a writeable file');
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.logger.warn('The output file already exists. It will be overwritten.');
|
||||
fs.unlinkSync(flags.output);
|
||||
}
|
||||
// this.logger.warn('The output file already exists. It will be overwritten.');
|
||||
// fs.unlinkSync(flags.output);
|
||||
// }
|
||||
|
||||
try {
|
||||
this.logger.info(`Processing prompt: ${flags.prompt}`);
|
||||
// try {
|
||||
// this.logger.info(`Processing prompt: ${flags.prompt}`);
|
||||
|
||||
const aiResponse = wfBuilder.chat({ question: flags.prompt });
|
||||
// const aiResponse = wfBuilder.chat({ question: flags.prompt });
|
||||
|
||||
const generatedWorkflow = await waitForWorkflowGenerated(aiResponse);
|
||||
// const generatedWorkflow = await waitForWorkflowGenerated(aiResponse);
|
||||
|
||||
this.logger.info(`Generated workflow for prompt: ${flags.prompt}`);
|
||||
// this.logger.info(`Generated workflow for prompt: ${flags.prompt}`);
|
||||
|
||||
if (flags.output) {
|
||||
fs.writeFileSync(flags.output, generatedWorkflow);
|
||||
this.logger.info(`Workflow saved to ${flags.output}`);
|
||||
} else {
|
||||
this.logger.info('Generated Workflow:');
|
||||
// Pretty print JSON
|
||||
this.logger.info(JSON.stringify(JSON.parse(generatedWorkflow), null, 2));
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'An error occurred';
|
||||
this.logger.error(`Error processing prompt "${flags.prompt}": ${errorMessage}`);
|
||||
}
|
||||
} else if (flags.input) {
|
||||
// Batch mode
|
||||
const output = flags.output ?? 'ttwf-results.jsonl';
|
||||
if (fs.existsSync(output)) {
|
||||
if (fs.lstatSync(output).isDirectory()) {
|
||||
this.logger.info('The parameter --output must be a writeable file');
|
||||
return;
|
||||
}
|
||||
// if (flags.output) {
|
||||
// fs.writeFileSync(flags.output, generatedWorkflow);
|
||||
// this.logger.info(`Workflow saved to ${flags.output}`);
|
||||
// } else {
|
||||
// this.logger.info('Generated Workflow:');
|
||||
// // Pretty print JSON
|
||||
// this.logger.info(JSON.stringify(JSON.parse(generatedWorkflow), null, 2));
|
||||
// }
|
||||
// } catch (e) {
|
||||
// const errorMessage = e instanceof Error ? e.message : 'An error occurred';
|
||||
// this.logger.error(`Error processing prompt "${flags.prompt}": ${errorMessage}`);
|
||||
// }
|
||||
// } else if (flags.input) {
|
||||
// // Batch mode
|
||||
// const output = flags.output ?? 'ttwf-results.jsonl';
|
||||
// if (fs.existsSync(output)) {
|
||||
// if (fs.lstatSync(output).isDirectory()) {
|
||||
// this.logger.info('The parameter --output must be a writeable file');
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.logger.warn('The output file already exists. It will be overwritten.');
|
||||
fs.unlinkSync(output);
|
||||
}
|
||||
// this.logger.warn('The output file already exists. It will be overwritten.');
|
||||
// fs.unlinkSync(output);
|
||||
// }
|
||||
|
||||
const pool = new WorkerPool<string>(flags.concurrency ?? 1);
|
||||
// const pool = new WorkerPool<string>(flags.concurrency ?? 1);
|
||||
|
||||
const dataset = await this.readDataset(flags.input);
|
||||
// const dataset = await this.readDataset(flags.input);
|
||||
|
||||
// Open file for writing results
|
||||
const outputStream = fs.createWriteStream(output, { flags: 'a' });
|
||||
// // Open file for writing results
|
||||
// const outputStream = fs.createWriteStream(output, { flags: 'a' });
|
||||
|
||||
const datasetWithLimit = (flags.limit ?? -1) > 0 ? dataset.slice(0, flags.limit) : dataset;
|
||||
// const datasetWithLimit = (flags.limit ?? -1) > 0 ? dataset.slice(0, flags.limit) : dataset;
|
||||
|
||||
await Promise.allSettled(
|
||||
datasetWithLimit.map(async (item) => {
|
||||
try {
|
||||
const generatedWorkflow = await pool.execute(async () => {
|
||||
this.logger.info(`Processing prompt: ${item.prompt}`);
|
||||
// await Promise.allSettled(
|
||||
// datasetWithLimit.map(async (item) => {
|
||||
// try {
|
||||
// const generatedWorkflow = await pool.execute(async () => {
|
||||
// this.logger.info(`Processing prompt: ${item.prompt}`);
|
||||
|
||||
const aiResponse = wfBuilder.chat({ question: item.prompt });
|
||||
// const aiResponse = wfBuilder.chat({ question: item.prompt });
|
||||
|
||||
return await waitForWorkflowGenerated(aiResponse);
|
||||
});
|
||||
// return await waitForWorkflowGenerated(aiResponse);
|
||||
// });
|
||||
|
||||
this.logger.info(`Generated workflow for prompt: ${item.prompt}`);
|
||||
// this.logger.info(`Generated workflow for prompt: ${item.prompt}`);
|
||||
|
||||
// Write the generated workflow to the output file
|
||||
outputStream.write(
|
||||
JSON.stringify({
|
||||
prompt: item.prompt,
|
||||
generatedWorkflow,
|
||||
referenceWorkflow: item.referenceWorkflow,
|
||||
}) + '\n',
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'An error occurred';
|
||||
this.logger.error(`Error processing prompt "${item.prompt}": ${errorMessage}`);
|
||||
// Optionally write the error to the output file
|
||||
outputStream.write(
|
||||
JSON.stringify({
|
||||
prompt: item.prompt,
|
||||
referenceWorkflow: item.referenceWorkflow,
|
||||
errorMessage,
|
||||
}) + '\n',
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
// // Write the generated workflow to the output file
|
||||
// outputStream.write(
|
||||
// JSON.stringify({
|
||||
// prompt: item.prompt,
|
||||
// generatedWorkflow,
|
||||
// referenceWorkflow: item.referenceWorkflow,
|
||||
// }) + '\n',
|
||||
// );
|
||||
// } catch (e) {
|
||||
// const errorMessage = e instanceof Error ? e.message : 'An error occurred';
|
||||
// this.logger.error(`Error processing prompt "${item.prompt}": ${errorMessage}`);
|
||||
// // Optionally write the error to the output file
|
||||
// outputStream.write(
|
||||
// JSON.stringify({
|
||||
// prompt: item.prompt,
|
||||
// referenceWorkflow: item.referenceWorkflow,
|
||||
// errorMessage,
|
||||
// }) + '\n',
|
||||
// );
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
|
||||
outputStream.end();
|
||||
}
|
||||
// outputStream.end();
|
||||
// }
|
||||
}
|
||||
|
||||
async catch(error: Error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AiAskRequestDto,
|
||||
AiApplySuggestionRequestDto,
|
||||
AiChatRequestDto,
|
||||
AiBuilderChatRequestDto,
|
||||
} from '@n8n/api-types';
|
||||
import type { AuthenticatedRequest } from '@n8n/db';
|
||||
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
|
||||
@@ -27,6 +28,7 @@ describe('AiController', () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
response.header.mockReturnThis();
|
||||
response.status.mockReturnThis();
|
||||
});
|
||||
|
||||
describe('chat', () => {
|
||||
@@ -110,4 +112,134 @@ describe('AiController', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
const payload: AiBuilderChatRequestDto = {
|
||||
payload: {
|
||||
text: 'Create a workflow',
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
workflowContext: {
|
||||
currentWorkflow: { id: 'workflow123' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should handle build request successfully', async () => {
|
||||
const mockChunks = [
|
||||
{ messages: [{ role: 'assistant', type: 'message', text: 'Building...' } as const] },
|
||||
{ messages: [{ role: 'assistant', type: 'workflow-updated', codeSnippet: '{}' } as const] },
|
||||
];
|
||||
|
||||
// Create an async generator that yields chunks
|
||||
async function* mockChatGenerator() {
|
||||
for (const chunk of mockChunks) {
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
workflowBuilderService.chat.mockReturnValue(mockChatGenerator());
|
||||
|
||||
await controller.build(request, response, payload);
|
||||
|
||||
expect(workflowBuilderService.chat).toHaveBeenCalledWith(
|
||||
{
|
||||
message: 'Create a workflow',
|
||||
workflowContext: {
|
||||
currentWorkflow: { id: 'workflow123' },
|
||||
executionData: undefined,
|
||||
executionSchema: undefined,
|
||||
},
|
||||
},
|
||||
request.user,
|
||||
);
|
||||
expect(response.header).toHaveBeenCalledWith('Content-type', 'application/json-lines');
|
||||
expect(response.flush).toHaveBeenCalled();
|
||||
expect(response.write).toHaveBeenCalledTimes(2);
|
||||
expect(response.write).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
JSON.stringify(mockChunks[0]) + '⧉⇋⇋➽⌑⧉§§\n',
|
||||
);
|
||||
expect(response.write).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
JSON.stringify(mockChunks[1]) + '⧉⇋⇋➽⌑⧉§§\n',
|
||||
);
|
||||
expect(response.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors during streaming and send error chunk', async () => {
|
||||
const mockError = new Error('Tool execution failed');
|
||||
|
||||
// Create an async generator that throws an error
|
||||
async function* mockChatGeneratorWithError() {
|
||||
yield { messages: [{ role: 'assistant', type: 'message', text: 'Starting...' } as const] };
|
||||
throw mockError;
|
||||
}
|
||||
|
||||
workflowBuilderService.chat.mockReturnValue(mockChatGeneratorWithError());
|
||||
|
||||
await controller.build(request, response, payload);
|
||||
|
||||
expect(workflowBuilderService.chat).toHaveBeenCalled();
|
||||
expect(response.header).toHaveBeenCalledWith('Content-type', 'application/json-lines');
|
||||
expect(response.write).toHaveBeenCalledTimes(2);
|
||||
// First chunk
|
||||
expect(response.write).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
JSON.stringify({
|
||||
messages: [{ role: 'assistant', type: 'message', text: 'Starting...' }],
|
||||
}) + '⧉⇋⇋➽⌑⧉§§\n',
|
||||
);
|
||||
// Error chunk
|
||||
expect(response.write).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
type: 'error',
|
||||
content: 'Tool execution failed',
|
||||
},
|
||||
],
|
||||
}) + '⧉⇋⇋➽⌑⧉§§\n',
|
||||
);
|
||||
expect(response.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors before streaming starts', async () => {
|
||||
const mockError = new Error('Failed to initialize');
|
||||
|
||||
workflowBuilderService.chat.mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
response.headersSent = false;
|
||||
|
||||
await controller.build(request, response, payload);
|
||||
|
||||
expect(response.status).toHaveBeenCalledWith(500);
|
||||
expect(response.json).toHaveBeenCalledWith({
|
||||
code: 500,
|
||||
message: 'Failed to initialize',
|
||||
});
|
||||
expect(response.write).not.toHaveBeenCalled();
|
||||
expect(response.end).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not try to send error response if headers already sent', async () => {
|
||||
const mockError = new Error('Failed after headers');
|
||||
|
||||
workflowBuilderService.chat.mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
response.headersSent = true;
|
||||
|
||||
await controller.build(request, response, payload);
|
||||
|
||||
expect(response.status).not.toHaveBeenCalled();
|
||||
expect(response.json).not.toHaveBeenCalled();
|
||||
expect(response.end).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AiAskRequestDto,
|
||||
AiFreeCreditsRequestDto,
|
||||
AiBuilderChatRequestDto,
|
||||
AiSessionRetrievalRequestDto,
|
||||
} from '@n8n/api-types';
|
||||
import { AuthenticatedRequest } from '@n8n/db';
|
||||
import { Body, Post, RestController } from '@n8n/decorators';
|
||||
@@ -35,32 +36,69 @@ export class AiController {
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Post('/build', { rateLimit: { limit: 100 } })
|
||||
// Use usesTemplates flag to bypass the send() wrapper which would cause
|
||||
// "Cannot set headers after they are sent" error for streaming responses.
|
||||
// This ensures errors during streaming are handled within the stream itself.
|
||||
@Post('/build', { rateLimit: { limit: 100 }, usesTemplates: true })
|
||||
async build(
|
||||
req: AuthenticatedRequest,
|
||||
res: FlushableResponse,
|
||||
@Body payload: AiBuilderChatRequestDto,
|
||||
) {
|
||||
try {
|
||||
const { text, workflowContext } = payload.payload;
|
||||
const aiResponse = this.workflowBuilderService.chat(
|
||||
{
|
||||
question: payload.payload.question ?? '',
|
||||
message: text,
|
||||
workflowContext: {
|
||||
currentWorkflow: workflowContext.currentWorkflow,
|
||||
executionData: workflowContext.executionData,
|
||||
executionSchema: workflowContext.executionSchema,
|
||||
},
|
||||
},
|
||||
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');
|
||||
try {
|
||||
// Handle the stream
|
||||
for await (const chunk of aiResponse) {
|
||||
res.flush();
|
||||
res.write(JSON.stringify(chunk) + '⧉⇋⇋➽⌑⧉§§\n');
|
||||
}
|
||||
} catch (streamError) {
|
||||
// If an error occurs during streaming, send it as part of the stream
|
||||
// This prevents "Cannot set headers after they are sent" error
|
||||
assert(streamError instanceof Error);
|
||||
|
||||
// Send error as proper error type now that frontend supports it
|
||||
const errorChunk = {
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
type: 'error',
|
||||
content: streamError.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
res.write(JSON.stringify(errorChunk) + '⧉⇋⇋➽⌑⧉§§\n');
|
||||
}
|
||||
|
||||
res.end();
|
||||
} catch (e) {
|
||||
// This catch block handles errors that occur before streaming starts
|
||||
// Since headers haven't been sent yet, we can still send a proper error response
|
||||
assert(e instanceof Error);
|
||||
throw new InternalServerError(e.message, e);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: e.message,
|
||||
});
|
||||
} else {
|
||||
// If headers were already sent dont't send a second error response
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,4 +195,19 @@ export class AiController {
|
||||
throw new InternalServerError(e.message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/sessions', { rateLimit: { limit: 100 } })
|
||||
async getSessions(
|
||||
req: AuthenticatedRequest,
|
||||
_: Response,
|
||||
@Body payload: AiSessionRetrievalRequestDto,
|
||||
) {
|
||||
try {
|
||||
const sessions = await this.workflowBuilderService.getSessions(payload.workflowId, req.user);
|
||||
return sessions;
|
||||
} catch (e) {
|
||||
assert(e instanceof Error);
|
||||
throw new InternalServerError(e.message, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AiWorkflowBuilderService } from '@n8n/ai-workflow-builder';
|
||||
import { ChatPayload } from '@n8n/ai-workflow-builder/dist/workflow-builder-agent';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk';
|
||||
@@ -20,6 +22,7 @@ export class WorkflowBuilderService {
|
||||
private readonly nodeTypes: NodeTypes,
|
||||
private readonly license: License,
|
||||
private readonly config: GlobalConfig,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
private async getService(): Promise<AiWorkflowBuilderService> {
|
||||
@@ -40,13 +43,19 @@ export class WorkflowBuilderService {
|
||||
});
|
||||
}
|
||||
|
||||
this.service = new AiWorkflowBuilderService(this.nodeTypes, client);
|
||||
this.service = new AiWorkflowBuilderService(this.nodeTypes, client, this.logger);
|
||||
}
|
||||
return this.service;
|
||||
}
|
||||
|
||||
async *chat(payload: { question: string }, user: IUser) {
|
||||
async *chat(payload: ChatPayload, user: IUser) {
|
||||
const service = await this.getService();
|
||||
yield* service.chat(payload, user);
|
||||
}
|
||||
|
||||
async getSessions(workflowId: string | undefined, user: IUser) {
|
||||
const service = await this.getService();
|
||||
const sessions = await service.getSessions(workflowId, user);
|
||||
return sessions;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user