mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat: Add workflow name generation and save after initial generation (no-changelog) (#18348)
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { PromptTemplate } from '@langchain/core/prompts';
|
||||
import z from 'zod';
|
||||
|
||||
const workflowNamingPromptTemplate = PromptTemplate.fromTemplate(
|
||||
`Based on the initial user prompt, please generate a name for the workflow that captures its essence and purpose.
|
||||
|
||||
<initial_prompt>
|
||||
{initialPrompt}
|
||||
</initial_prompt>
|
||||
|
||||
This name should be concise, descriptive, and suitable for a workflow that automates tasks related to the given prompt. The name should be in a format that is easy to read and understand.
|
||||
`,
|
||||
);
|
||||
|
||||
export async function workflowNameChain(llm: BaseChatModel, initialPrompt: string) {
|
||||
// Use structured output for the workflow name to ensure it meets the required format and length
|
||||
const nameSchema = z.object({
|
||||
name: z.string().min(10).max(128).describe('Name of the workflow based on the prompt'),
|
||||
});
|
||||
|
||||
const modelWithStructure = llm.withStructuredOutput(nameSchema);
|
||||
|
||||
const prompt = await workflowNamingPromptTemplate.invoke({
|
||||
initialPrompt,
|
||||
});
|
||||
|
||||
const structuredOutput = (await modelWithStructure.invoke(prompt)) as z.infer<typeof nameSchema>;
|
||||
|
||||
return {
|
||||
name: structuredOutput.name,
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { IWorkflowBase, INode, IConnections } from 'n8n-workflow';
|
||||
/**
|
||||
* Simplified workflow representation containing only nodes and connections
|
||||
*/
|
||||
export type SimpleWorkflow = Pick<IWorkflowBase, 'nodes' | 'connections'>;
|
||||
export type SimpleWorkflow = Pick<IWorkflowBase, 'name' | 'nodes' | 'connections'>;
|
||||
|
||||
/**
|
||||
* Workflow operation types that can be applied to the workflow state
|
||||
@@ -14,4 +14,5 @@ export type WorkflowOperation =
|
||||
| { type: 'addNodes'; nodes: INode[] }
|
||||
| { type: 'updateNode'; nodeId: string; updates: Partial<INode> }
|
||||
| { type: 'setConnections'; connections: IConnections }
|
||||
| { type: 'mergeConnections'; connections: IConnections };
|
||||
| { type: 'mergeConnections'; connections: IConnections }
|
||||
| { type: 'setName'; name: string };
|
||||
|
||||
@@ -15,13 +15,14 @@ export function applyOperations(
|
||||
let result: SimpleWorkflow = {
|
||||
nodes: [...workflow.nodes],
|
||||
connections: { ...workflow.connections },
|
||||
name: workflow.name || '',
|
||||
};
|
||||
|
||||
// Apply each operation in sequence
|
||||
for (const operation of operations) {
|
||||
switch (operation.type) {
|
||||
case 'clear':
|
||||
result = { nodes: [], connections: {} };
|
||||
result = { nodes: [], connections: {}, name: '' };
|
||||
break;
|
||||
|
||||
case 'removeNode': {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('operations-processor', () => {
|
||||
node3 = createNode({ id: 'node3', name: 'Node 3', position: [500, 100] });
|
||||
|
||||
baseWorkflow = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [node1, node2, node3],
|
||||
connections: {
|
||||
node1: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
NodeExecutionSchema,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { workflowNameChain } from '@/chains/workflow-name';
|
||||
import { DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS, MAX_AI_BUILDER_PROMPT_LENGTH } from '@/constants';
|
||||
|
||||
import { conversationCompactChain } from './chains/conversation-compact';
|
||||
@@ -122,7 +123,7 @@ export class WorkflowBuilderAgent {
|
||||
};
|
||||
|
||||
const shouldModifyState = (state: typeof WorkflowState.State) => {
|
||||
const { messages } = state;
|
||||
const { messages, workflowContext } = state;
|
||||
const lastHumanMessage = messages.findLast((m) => m instanceof HumanMessage)!; // There always should be at least one human message in the array
|
||||
|
||||
if (lastHumanMessage.content === '/compact') {
|
||||
@@ -133,6 +134,12 @@ export class WorkflowBuilderAgent {
|
||||
return 'delete_messages';
|
||||
}
|
||||
|
||||
// If the workflow is empty (no nodes),
|
||||
// we consider it initial generation request and auto-generate a name for the workflow.
|
||||
if (workflowContext?.currentWorkflow?.nodes?.length === 0 && messages.length === 1) {
|
||||
return 'create_workflow_name';
|
||||
}
|
||||
|
||||
if (shouldAutoCompact(state)) {
|
||||
return 'auto_compact_messages';
|
||||
}
|
||||
@@ -162,6 +169,7 @@ export class WorkflowBuilderAgent {
|
||||
workflowJSON: {
|
||||
nodes: [],
|
||||
connections: {},
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,7 +188,7 @@ export class WorkflowBuilderAgent {
|
||||
}
|
||||
|
||||
const { messages, previousSummary } = state;
|
||||
const lastHumanMessage = messages[messages.length - 1] as HumanMessage;
|
||||
const lastHumanMessage = messages[messages.length - 1] satisfies HumanMessage;
|
||||
const isAutoCompact = lastHumanMessage.content !== '/compact';
|
||||
|
||||
this.logger?.debug('Compacting conversation history', {
|
||||
@@ -209,6 +217,40 @@ export class WorkflowBuilderAgent {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a workflow name based on the initial user message.
|
||||
*/
|
||||
const createWorkflowName = async (state: typeof WorkflowState.State) => {
|
||||
if (!this.llmSimpleTask) {
|
||||
throw new LLMServiceError('LLM not setup');
|
||||
}
|
||||
|
||||
const { workflowJSON, messages } = state;
|
||||
|
||||
if (messages.length === 1 && messages[0] instanceof HumanMessage) {
|
||||
const initialMessage = messages[0] satisfies HumanMessage;
|
||||
|
||||
if (typeof initialMessage.content !== 'string') {
|
||||
this.logger?.debug(
|
||||
'Initial message content is not a string, skipping workflow name generation',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
this.logger?.debug('Generating workflow name');
|
||||
const { name } = await workflowNameChain(this.llmSimpleTask, initialMessage.content);
|
||||
|
||||
return {
|
||||
workflowJSON: {
|
||||
...workflowJSON,
|
||||
name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const workflow = new StateGraph(WorkflowState)
|
||||
.addNode('agent', callModel)
|
||||
.addNode('tools', customToolExecutor)
|
||||
@@ -216,10 +258,12 @@ export class WorkflowBuilderAgent {
|
||||
.addNode('delete_messages', deleteMessages)
|
||||
.addNode('compact_messages', compactSession)
|
||||
.addNode('auto_compact_messages', compactSession)
|
||||
.addNode('create_workflow_name', createWorkflowName)
|
||||
.addConditionalEdges('__start__', shouldModifyState)
|
||||
.addEdge('tools', 'process_operations')
|
||||
.addEdge('process_operations', 'agent')
|
||||
.addEdge('auto_compact_messages', 'agent')
|
||||
.addEdge('create_workflow_name', 'agent')
|
||||
.addEdge('delete_messages', END)
|
||||
.addEdge('compact_messages', END)
|
||||
.addConditionalEdges('agent', shouldContinue);
|
||||
|
||||
@@ -68,7 +68,7 @@ export const WorkflowState = Annotation.Root({
|
||||
// Now a simple field without custom reducer - all updates go through operations
|
||||
workflowJSON: Annotation<SimpleWorkflow>({
|
||||
reducer: (x, y) => y ?? x,
|
||||
default: () => ({ nodes: [], connections: {} }),
|
||||
default: () => ({ nodes: [], connections: {}, name: '' }),
|
||||
}),
|
||||
// Operations to apply to the workflow - processed by a separate node
|
||||
workflowOperations: Annotation<WorkflowOperation[] | null>({
|
||||
|
||||
@@ -44,7 +44,7 @@ export const createNode = (overrides: Partial<INode> = {}): INode => ({
|
||||
|
||||
// Simple workflow builder
|
||||
export const createWorkflow = (nodes: INode[] = []): SimpleWorkflow => {
|
||||
const workflow: SimpleWorkflow = { nodes, connections: {} };
|
||||
const workflow: SimpleWorkflow = { nodes, connections: {}, name: 'Test workflow' };
|
||||
return workflow;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user