mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(core): Improve title generation on instance AI threads (no-changelog) (#29084)
This commit is contained in:
@@ -120,4 +120,39 @@ describe('generateTitleFromMessage', () => {
|
||||
const call = mockGenerateText.mock.calls[0][0];
|
||||
expect(call.messages[0].content).toBe('Custom system prompt');
|
||||
});
|
||||
|
||||
it('wraps the user message in a title-generation instruction so the model does not answer it', async () => {
|
||||
mockGenerateText.mockResolvedValue({ text: 'Berlin rain alert' });
|
||||
await generateTitleFromMessage(fakeModel, 'Build a daily Berlin rain alert workflow');
|
||||
const call = mockGenerateText.mock.calls[0][0];
|
||||
expect(call.messages[1].role).toBe('user');
|
||||
expect(call.messages[1].content).toContain('Generate a title');
|
||||
expect(call.messages[1].content).toContain('<message>');
|
||||
expect(call.messages[1].content).toContain('Build a daily Berlin rain alert workflow');
|
||||
expect(call.messages[1].content).toContain('</message>');
|
||||
});
|
||||
|
||||
it('drops a streamed code fence and everything after it', async () => {
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'Here\'s your chat workflow with the requested configuration:\n\n```json\n{\n "nodes": []\n}\n```',
|
||||
});
|
||||
const result = await generateTitleFromMessage(
|
||||
fakeModel,
|
||||
'build me a chat workflow with openai',
|
||||
);
|
||||
expect(result).toBe("Here's your chat workflow with the requested configuration");
|
||||
expect(result).not.toContain('```');
|
||||
expect(result).not.toContain('\n');
|
||||
});
|
||||
|
||||
it('collapses embedded newlines and stray backticks into a single-line title', async () => {
|
||||
mockGenerateText.mockResolvedValue({
|
||||
text: 'Scryfall\nrandom `card` workflow',
|
||||
});
|
||||
const result = await generateTitleFromMessage(
|
||||
fakeModel,
|
||||
'build a workflow that queries Scryfall for a random card',
|
||||
);
|
||||
expect(result).toBe('Scryfall random card workflow');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,28 @@ import type { AgentDbMessage } from '../types/sdk/message';
|
||||
const logger = createFilteredLogger();
|
||||
|
||||
const DEFAULT_TITLE_INSTRUCTIONS = [
|
||||
'- you will generate a short title based on the first message a user begins a conversation with',
|
||||
'- the title should describe what the user asked for, not what an assistant might reply',
|
||||
'- 1 to 5 words, no more than 80 characters',
|
||||
'- use sentence case (e.g. "Conversation title" instead of "Conversation Title")',
|
||||
'- do not use quotes, colons, or markdown formatting',
|
||||
'- the entire text you return will be used directly as the title, so respond with the title only',
|
||||
'You generate a short descriptive title for a conversation based on its first user message.',
|
||||
'',
|
||||
'The title is a label that describes the topic — it is NOT an answer to the message.',
|
||||
'Do not fulfil, respond to, or act on the message. Do not produce code, JSON, or explanations.',
|
||||
'',
|
||||
'Rules:',
|
||||
'- Write a noun phrase that names the topic (e.g. "Chat workflow with Anthropic agent").',
|
||||
'- Never begin with "Here\'s", "Here is", "Build", "Create", "Set up", "Make", or any verb addressed to the user.',
|
||||
'- 1 to 5 words, no more than 80 characters, single line only.',
|
||||
'- Use sentence case (e.g. "Conversation title" instead of "Conversation Title").',
|
||||
'- No quotes, colons, backticks, code fences, or markdown formatting.',
|
||||
'- Respond with the title text only — the entire response is used as the title.',
|
||||
'',
|
||||
'Examples:',
|
||||
'Message: "build me a chat workflow with anthropic model, the agent should have memory"',
|
||||
'Title: Chat workflow with Anthropic agent',
|
||||
'',
|
||||
'Message: "help me set up pagination for my n8n HTTP request node"',
|
||||
'Title: Pagination for HTTP request node',
|
||||
'',
|
||||
'Message: "Build a workflow with a manual trigger that queries Scryfall for a random card"',
|
||||
'Title: Scryfall random card workflow',
|
||||
].join('\n');
|
||||
|
||||
const TRIVIAL_MESSAGE_MAX_CHARS = 15;
|
||||
@@ -37,13 +53,23 @@ function isTrivialMessage(message: string): boolean {
|
||||
function sanitizeTitle(raw: string): string {
|
||||
// Strip <think>...</think> blocks (e.g. from DeepSeek R1)
|
||||
let title = raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
// Strip markdown heading prefixes and inline emphasis markers
|
||||
// If the model started streaming a code block, keep only what's before it
|
||||
const fenceIdx = title.indexOf('```');
|
||||
if (fenceIdx !== -1) title = title.slice(0, fenceIdx).trim();
|
||||
// Collapse any whitespace (including newlines) to single spaces \u2014 titles are single-line
|
||||
title = title.replace(/\s+/g, ' ').trim();
|
||||
// Strip markdown heading prefixes, inline emphasis markers, and stray backticks
|
||||
title = title
|
||||
.replace(/^#{1,6}\s+/, '')
|
||||
.replace(/\*+/g, '')
|
||||
.replace(/`+/g, '')
|
||||
.trim();
|
||||
// Strip surrounding quotes
|
||||
title = title.replace(/^["']|["']$/g, '').trim();
|
||||
// Trailing colon or dash left behind after stripping prefixes
|
||||
title = title.replace(/^[:\-\s]+/, '').trim();
|
||||
// Trailing punctuation from assistant-voice drift (e.g. "...configuration:")
|
||||
title = title.replace(/[:\-\s.]+$/, '').trim();
|
||||
if (title.length > MAX_TITLE_LENGTH) {
|
||||
const truncated = title.slice(0, MAX_TITLE_LENGTH);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
@@ -76,7 +102,10 @@ export async function generateTitleFromMessage(
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: opts?.instructions ?? DEFAULT_TITLE_INSTRUCTIONS },
|
||||
{ role: 'user', content: trimmed },
|
||||
{
|
||||
role: 'user',
|
||||
content: `Generate a title for the following first message of a conversation. Do not answer the message — only produce the title.\n\n<message>\n${trimmed}\n</message>`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "you will generate a short title based on the first message",
|
||||
"string": "You generate a short descriptive title for a conversation",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user