mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat: Decrease AI Builder wordiness (no-changelog) (#18920)
This commit is contained in:
@@ -161,6 +161,7 @@ The AI Workflow Builder agent needs access to node definitions to generate workf
|
||||
1. Run your n8n instance
|
||||
2. Download the node definitions from locally running n8n instance(http://localhost:5678/types/nodes.json)
|
||||
3. Save the node definitions to `evaluations/nodes.json`
|
||||
` curl -o evaluations/nodes.json http://localhost:5678/types/nodes.json`
|
||||
|
||||
The evaluation will fail with a clear error message if `nodes.json` is missing.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { findNodeType } from './helpers/validation';
|
||||
import type { AddedNode } from '../types/nodes';
|
||||
import type { AddNodeOutput, ToolError } from '../types/tools';
|
||||
|
||||
const DISPLAY_TITLE = 'Adding node';
|
||||
const DISPLAY_TITLE = 'Adding nodes';
|
||||
|
||||
/**
|
||||
* Schema for node creation input
|
||||
@@ -77,7 +77,8 @@ function getCustomNodeTitle(
|
||||
}
|
||||
}
|
||||
|
||||
return DISPLAY_TITLE;
|
||||
// single "node" not plural "nodes" because this pertains to this specific tool call
|
||||
return 'Adding node';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,26 +3,21 @@ import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { instanceUrlPrompt } from '@/chains/prompts/instance-url';
|
||||
|
||||
const systemPrompt = `You are an AI assistant specialized in creating and editing n8n workflows. Your goal is to help users build efficient, well-connected workflows by intelligently using the available tools.
|
||||
|
||||
<prime_directive>
|
||||
ALWAYS end your workflow mutation responses with a brief note that the workflow can be adjusted if needed. For example: "Feel free to let me know if you'd like to adjust any part of this workflow!" This is mandatory for all workflow mutation responses.
|
||||
</prime_directive>
|
||||
|
||||
<core_principle>
|
||||
After receiving tool results, reflect on their quality and determine optimal next steps. Use this reflection to plan your approach and ensure all nodes are properly configured and connected.
|
||||
</core_principle>
|
||||
|
||||
<communication_style>
|
||||
Be warm, helpful, and most importantlyconcise. Focus on actionable information.
|
||||
- Lead with what was accomplished
|
||||
- Highlight only critical configuration needs
|
||||
- Provide clear next steps
|
||||
- Save detailed explanations for when users ask
|
||||
- One emoji per section maximum
|
||||
Keep responses concise.
|
||||
|
||||
CRITICAL: Do NOT provide commentary between tool calls. Execute tools silently.
|
||||
- NO progress messages like "Perfect!", "Now let me...", "Excellent!"
|
||||
- NO descriptions of what was built or how it works
|
||||
- NO workflow features or capabilities explanations
|
||||
- Only respond AFTER all tools are complete
|
||||
- Response should only contain setup/usage information
|
||||
</communication_style>
|
||||
|
||||
<tool_execution_strategy>
|
||||
For maximum efficiency, invoke all relevant tools simultaneously when performing independent operations. This significantly reduces wait time and improves user experience.
|
||||
|
||||
Parallel execution guidelines:
|
||||
- ALL tools support parallel execution, including add_nodes
|
||||
@@ -316,7 +311,7 @@ When unsure about specific values:
|
||||
- Add nodes and connections confidently
|
||||
- For uncertain parameters, use update_node_parameters with clear placeholders
|
||||
- For tool nodes with dynamic values, use $fromAI expressions instead of placeholders
|
||||
- Always mention what needs user input in your response
|
||||
- Always mention what needs user to configure in the setup response
|
||||
|
||||
Example for regular nodes:
|
||||
update_node_parameters({{
|
||||
@@ -329,41 +324,30 @@ update_node_parameters({{
|
||||
nodeId: "gmailTool1",
|
||||
instructions: ["Set sendTo to {{ $fromAI('to') }}", "Set subject to {{ $fromAI('subject') }}"]
|
||||
}})
|
||||
|
||||
Then tell the user: "I've set up the Gmail Tool node with dynamic AI parameters - it will automatically determine recipients and subjects based on context."
|
||||
</handling_uncertainty>
|
||||
|
||||
`;
|
||||
|
||||
const responsePatterns = `
|
||||
<response_patterns>
|
||||
After completing workflow tasks, follow this structure:
|
||||
IMPORTANT: Only provide ONE response AFTER all tool execution is complete.
|
||||
|
||||
1. **Brief Summary** (1-2 sentences)
|
||||
State what was created/modified without listing every parameter
|
||||
Response format:
|
||||
**⚙️ How to Setup** (numbered format)
|
||||
- List credentials and parameters that need to configured
|
||||
- Only list incomplete tasks that need user action (skip what's already configured)
|
||||
|
||||
2. **Key Requirements** (if any)
|
||||
- Credentials needed
|
||||
- Parameters the user should verify
|
||||
- Any manual configuration required
|
||||
**ℹ️ How to Use**
|
||||
- Only essential user actions (what to click, where to go)
|
||||
|
||||
3. **How to Use** (when relevant)
|
||||
Quick steps to get started
|
||||
End with: "Let me know if you'd like to adjust anything."
|
||||
|
||||
4. **Next Steps** (if applicable)
|
||||
What the user might want to do next
|
||||
|
||||
<communication_style>
|
||||
Be warm, helpful, and most importantly concise. Focus on actionable information.
|
||||
- Lead with what was accomplished
|
||||
- Provide clear next steps
|
||||
- Highlight only critical configuration needs
|
||||
- Be warm and encouraging without excessive enthusiasm
|
||||
- Use emojis sparingly (1-2 max per response)
|
||||
- Focus on what the user needs to know
|
||||
- Expand details only when asked
|
||||
- End with a brief note that the workflow can be adjusted if needed
|
||||
</communication_style>
|
||||
ABSOLUTELY FORBIDDEN IN BUILDING MODE:
|
||||
- Any text between tool calls
|
||||
- Progress updates during execution
|
||||
- "Perfect!", "Now let me...", "Excellent!"
|
||||
- Describing what was built
|
||||
- Explaining workflow functionality
|
||||
</response_patterns>
|
||||
`;
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ export function processStreamChunk(streamMode: string, chunk: unknown): StreamOu
|
||||
if ((agentChunk?.compact_messages?.messages ?? []).length > 0) {
|
||||
const lastMessage =
|
||||
agentChunk.compact_messages!.messages![agentChunk.compact_messages!.messages!.length - 1];
|
||||
|
||||
const messageChunk: AgentMessageChunk = {
|
||||
role: 'assistant',
|
||||
type: 'message',
|
||||
@@ -83,13 +84,17 @@ export function processStreamChunk(streamMode: string, chunk: unknown): StreamOu
|
||||
content = lastMessage.content;
|
||||
}
|
||||
|
||||
const messageChunk: AgentMessageChunk = {
|
||||
role: 'assistant',
|
||||
type: 'message',
|
||||
text: content,
|
||||
};
|
||||
if (content) {
|
||||
const messageChunk: AgentMessageChunk = {
|
||||
role: 'assistant',
|
||||
type: 'message',
|
||||
text: content,
|
||||
};
|
||||
|
||||
return { messages: [messageChunk] };
|
||||
return { messages: [messageChunk] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,18 @@ describe('stream-processor', () => {
|
||||
expect(message.text).toBe('Last message to display');
|
||||
});
|
||||
|
||||
it('should handle compact_messages with empty content', () => {
|
||||
const chunk = {
|
||||
agent: {
|
||||
messages: [{ content: 'First message' }, { content: [{ type: 'text', text: '' }] }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = processStreamChunk('updates', chunk);
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it('should handle process_operations with workflow update', () => {
|
||||
const workflowData = {
|
||||
nodes: [{ id: 'node1', name: 'Test Node' }],
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { render } from '@testing-library/vue';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { n8nHtml } from '@n8n/design-system/directives';
|
||||
|
||||
import AskAssistantChat from './AskAssistantChat.vue';
|
||||
import { n8nHtml } from '../../directives';
|
||||
import type { Props as MessageWrapperProps } from './messages/MessageWrapper.vue';
|
||||
import type { ChatUI } from '../../types/assistant';
|
||||
|
||||
@@ -388,6 +387,70 @@ describe('AskAssistantChat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should collapse tool messages with same toolName with hidden messages in between', () => {
|
||||
const messages: Array<ChatUI.AssistantMessage & { id: string }> = [
|
||||
createToolMessage({
|
||||
id: '1',
|
||||
status: 'running',
|
||||
displayTitle: 'Searching...',
|
||||
updates: [{ type: 'progress', data: { status: 'Initializing search' } }],
|
||||
}),
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant',
|
||||
type: 'agent-suggestion',
|
||||
title: 'Agent Suggestion',
|
||||
content: 'This is a suggestion from the agent',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [
|
||||
{ type: 'accept', text: 'Accept suggestion' },
|
||||
{ type: 'reject', text: 'Reject suggestion' },
|
||||
],
|
||||
read: true,
|
||||
},
|
||||
createToolMessage({
|
||||
id: '2',
|
||||
status: 'running',
|
||||
displayTitle: 'Still searching...',
|
||||
customDisplayTitle: 'Custom Search Title',
|
||||
updates: [{ type: 'progress', data: { status: 'Processing results' } }],
|
||||
}),
|
||||
{
|
||||
id: 'test',
|
||||
role: 'assistant',
|
||||
type: 'workflow-updated',
|
||||
codeSnippet: '',
|
||||
},
|
||||
createToolMessage({
|
||||
id: '3',
|
||||
status: 'completed',
|
||||
displayTitle: 'Search Complete',
|
||||
updates: [{ type: 'output', data: { result: 'Found 10 items' } }],
|
||||
}),
|
||||
];
|
||||
|
||||
renderWithMessages(messages);
|
||||
|
||||
expectMessageWrapperCalledTimes(1);
|
||||
const props = getMessageWrapperProps();
|
||||
|
||||
expectToolMessage(props, {
|
||||
id: '3',
|
||||
role: 'assistant',
|
||||
type: 'tool',
|
||||
toolName: 'search',
|
||||
status: 'running',
|
||||
displayTitle: 'Still searching...',
|
||||
customDisplayTitle: 'Custom Search Title',
|
||||
updates: [
|
||||
{ type: 'progress', data: { status: 'Initializing search' } },
|
||||
{ type: 'progress', data: { status: 'Processing results' } },
|
||||
{ type: 'output', data: { result: 'Found 10 items' } },
|
||||
],
|
||||
read: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not collapse tool messages with different toolNames', () => {
|
||||
const messages = [
|
||||
createToolMessage({
|
||||
@@ -738,4 +801,184 @@ describe('AskAssistantChat', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quick Replies', () => {
|
||||
const renderWithQuickReplies = (
|
||||
messages: ChatUI.AssistantMessage[],
|
||||
streaming = false,
|
||||
loadingMessage?: string,
|
||||
) => {
|
||||
return render(AskAssistantChat, {
|
||||
global: {
|
||||
directives: { n8nHtml },
|
||||
stubs: {
|
||||
...Object.fromEntries(stubs.map((stub) => [stub, true])),
|
||||
'n8n-button': { template: '<button><slot></button' },
|
||||
},
|
||||
},
|
||||
props: {
|
||||
user: { firstName: 'Kobi', lastName: 'Dog' },
|
||||
messages,
|
||||
streaming,
|
||||
loadingMessage,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('should render quick replies for code-diff message', () => {
|
||||
const messages: ChatUI.AssistantMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
type: 'text',
|
||||
content: 'Here is a solution',
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant',
|
||||
type: 'code-diff',
|
||||
description: 'Code solution',
|
||||
codeDiff: 'diff content',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [
|
||||
{ type: 'new-suggestion', text: 'Give me another solution' },
|
||||
{ type: 'resolved', text: 'All good' },
|
||||
],
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = renderWithQuickReplies(messages);
|
||||
|
||||
// Quick replies should be rendered (2 buttons found)
|
||||
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(2);
|
||||
// Quick reply title should be visible
|
||||
expect(wrapper.container.textContent).toContain('Quick reply');
|
||||
expect(wrapper.container).toHaveTextContent('Give me another solution');
|
||||
expect(wrapper.container).toHaveTextContent('All good');
|
||||
});
|
||||
|
||||
it('should render quick replies for agent-suggestion messages', () => {
|
||||
const messages: ChatUI.AssistantMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
type: 'text',
|
||||
content: 'Here is a solution',
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant',
|
||||
type: 'agent-suggestion',
|
||||
title: 'Agent Suggestion',
|
||||
content: 'This is a suggestion from the agent',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [
|
||||
{ type: 'accept', text: 'Accept suggestion' },
|
||||
{ type: 'reject', text: 'Reject suggestion' },
|
||||
],
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = renderWithQuickReplies(messages);
|
||||
|
||||
// Quick replies should still be rendered even though agent-suggestion is filtered out
|
||||
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(2);
|
||||
// Quick reply title should be visible
|
||||
expect(wrapper.container.textContent).toContain('Quick reply');
|
||||
|
||||
expect(wrapper.container).toHaveTextContent('Accept suggestion');
|
||||
expect(wrapper.container).toHaveTextContent('Reject suggestion');
|
||||
});
|
||||
|
||||
it('should not render quick replies when streaming', () => {
|
||||
const messages: ChatUI.AssistantMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
type: 'code-diff',
|
||||
description: 'Code solution',
|
||||
codeDiff: 'diff content',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [{ type: 'new-suggestion', text: 'Give me another solution' }],
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = renderWithQuickReplies(messages, true);
|
||||
|
||||
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
|
||||
expect(wrapper.container.textContent).not.toContain('Quick reply');
|
||||
expect(wrapper.container.textContent).not.toContain('Give me another solution');
|
||||
});
|
||||
|
||||
it('should not render quick replies for non-last messages', () => {
|
||||
const messages: ChatUI.AssistantMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
type: 'code-diff',
|
||||
description: 'Code solution',
|
||||
codeDiff: 'diff content',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [{ type: 'new-suggestion', text: 'Give me another solution' }],
|
||||
read: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant',
|
||||
type: 'text',
|
||||
content: 'Follow up message',
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = renderWithQuickReplies(messages);
|
||||
|
||||
// Quick replies should not be rendered since the message with quick replies is not last
|
||||
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
|
||||
expect(wrapper.container.textContent).not.toContain('Quick reply');
|
||||
expect(wrapper.container.textContent).not.toContain('Give me another solution');
|
||||
});
|
||||
|
||||
it('should not render quick replies when last message has no quickReplies', () => {
|
||||
const messages: ChatUI.AssistantMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
type: 'text',
|
||||
content: 'Simple text message',
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = renderWithQuickReplies(messages);
|
||||
|
||||
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
|
||||
expect(wrapper.container.textContent).not.toContain('Quick reply');
|
||||
});
|
||||
|
||||
it('should not render quick replies when last message has empty quickReplies array', () => {
|
||||
const messages: ChatUI.AssistantMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
type: 'code-diff',
|
||||
description: 'Code solution',
|
||||
codeDiff: 'diff content',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [],
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = renderWithQuickReplies(messages);
|
||||
|
||||
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
|
||||
expect(wrapper.container.textContent).not.toContain('Quick reply');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
|
||||
import N8nButton from '../N8nButton';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import { getSupportedMessageComponent } from './messages/helpers';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -63,6 +64,11 @@ function normalizeMessages(messages: ChatUI.AssistantMessage[]): ChatUI.Assistan
|
||||
}));
|
||||
}
|
||||
|
||||
// filter out these messages so that tool collapsing works correctly
|
||||
function filterOutHiddenMessages(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
||||
return messages.filter((message) => Boolean(getSupportedMessageComponent(message.type)));
|
||||
}
|
||||
|
||||
function collapseToolMessages(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
||||
const result: ChatUI.AssistantMessage[] = [];
|
||||
let i = 0;
|
||||
@@ -136,7 +142,17 @@ function collapseToolMessages(messages: ChatUI.AssistantMessage[]): ChatUI.Assis
|
||||
// Ensure all messages have required id and read properties, and collapse tool messages
|
||||
const normalizedMessages = computed(() => {
|
||||
const normalized = normalizeMessages(props.messages);
|
||||
return collapseToolMessages(normalized);
|
||||
return collapseToolMessages(filterOutHiddenMessages(normalized));
|
||||
});
|
||||
|
||||
// Get quickReplies from the last message in the original messages (before filtering)
|
||||
const lastMessageQuickReplies = computed(() => {
|
||||
if (!props.messages?.length || props.streaming) return [];
|
||||
|
||||
const lastMessage = props.messages[props.messages.length - 1];
|
||||
return 'quickReplies' in lastMessage && lastMessage.quickReplies?.length
|
||||
? lastMessage.quickReplies
|
||||
: [];
|
||||
});
|
||||
|
||||
const textInputValue = ref<string>('');
|
||||
@@ -252,18 +268,17 @@ watch(
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
!streaming &&
|
||||
'quickReplies' in message &&
|
||||
message.quickReplies?.length &&
|
||||
i === normalizedMessages.length - 1
|
||||
"
|
||||
v-if="lastMessageQuickReplies.length && i === normalizedMessages.length - 1"
|
||||
:class="$style.quickReplies"
|
||||
>
|
||||
<div :class="$style.quickRepliesTitle">
|
||||
{{ t('assistantChat.quickRepliesTitle') }}
|
||||
</div>
|
||||
<div v-for="opt in message.quickReplies" :key="opt.type" data-test-id="quick-replies">
|
||||
<div
|
||||
v-for="opt in lastMessageQuickReplies"
|
||||
:key="opt.type"
|
||||
data-test-id="quick-replies"
|
||||
>
|
||||
<N8nButton
|
||||
v-if="opt.text"
|
||||
type="secondary"
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue';
|
||||
|
||||
import BlockMessage from './BlockMessage.vue';
|
||||
import CodeDiffMessage from './CodeDiffMessage.vue';
|
||||
import ErrorMessage from './ErrorMessage.vue';
|
||||
import EventMessage from './EventMessage.vue';
|
||||
import TextMessage from './TextMessage.vue';
|
||||
import ToolMessage from './ToolMessage.vue';
|
||||
import { getSupportedMessageComponent } from './helpers';
|
||||
import type { ChatUI, RatingFeedback } from '../../../types/assistant';
|
||||
|
||||
export interface Props {
|
||||
@@ -31,25 +26,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const messageComponent = computed<Component | null>(() => {
|
||||
switch (props.message.type) {
|
||||
case 'text':
|
||||
return TextMessage;
|
||||
case 'block':
|
||||
return BlockMessage;
|
||||
case 'code-diff':
|
||||
return CodeDiffMessage;
|
||||
case 'error':
|
||||
return ErrorMessage;
|
||||
case 'event':
|
||||
return EventMessage;
|
||||
case 'tool':
|
||||
return ToolMessage;
|
||||
case 'agent-suggestion':
|
||||
case 'workflow-updated':
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return getSupportedMessageComponent(props.message.type);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ChatUI } from '@n8n/design-system/types';
|
||||
|
||||
import BlockMessage from './BlockMessage.vue';
|
||||
import CodeDiffMessage from './CodeDiffMessage.vue';
|
||||
import ErrorMessage from './ErrorMessage.vue';
|
||||
import EventMessage from './EventMessage.vue';
|
||||
import TextMessage from './TextMessage.vue';
|
||||
import ToolMessage from './ToolMessage.vue';
|
||||
|
||||
export function getSupportedMessageComponent(type: ChatUI.AssistantMessage['type']) {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return TextMessage;
|
||||
case 'block':
|
||||
return BlockMessage;
|
||||
case 'code-diff':
|
||||
return CodeDiffMessage;
|
||||
case 'error':
|
||||
return ErrorMessage;
|
||||
case 'event':
|
||||
return EventMessage;
|
||||
case 'tool':
|
||||
return ToolMessage;
|
||||
case 'agent-suggestion':
|
||||
case 'workflow-updated':
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user