From f27bbc90226b3f370bb9f0103d742e7743fa5ce6 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Fri, 24 Apr 2026 18:55:13 +0300 Subject: [PATCH] fix(core): Improve title generation on instance AI threads (no-changelog) (#29084) --- .../src/__tests__/title-generation.test.ts | 35 +++++++++++++++ .../agents/src/runtime/title-generation.ts | 45 +++++++++++++++---- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- ...known-host-POST-_v1_messages-18622610.json | 2 +- 14 files changed, 84 insertions(+), 20 deletions(-) diff --git a/packages/@n8n/agents/src/__tests__/title-generation.test.ts b/packages/@n8n/agents/src/__tests__/title-generation.test.ts index f575b67b7ca..fdee4af6e19 100644 --- a/packages/@n8n/agents/src/__tests__/title-generation.test.ts +++ b/packages/@n8n/agents/src/__tests__/title-generation.test.ts @@ -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(''); + expect(call.messages[1].content).toContain('Build a daily Berlin rain alert workflow'); + expect(call.messages[1].content).toContain(''); + }); + + 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'); + }); }); diff --git a/packages/@n8n/agents/src/runtime/title-generation.ts b/packages/@n8n/agents/src/runtime/title-generation.ts index 7aa5480d86d..9aa29a7afda 100644 --- a/packages/@n8n/agents/src/runtime/title-generation.ts +++ b/packages/@n8n/agents/src/runtime/title-generation.ts @@ -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 ... blocks (e.g. from DeepSeek R1) let title = raw.replace(/[\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\n${trimmed}\n`, + }, ], }); diff --git a/packages/testing/playwright/expectations/instance-ai/should-auto-open-preview-panel-when-workflow-is-built/1775805992921-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-auto-open-preview-panel-when-workflow-is-built/1775805992921-unknown-host-POST-_v1_messages-18622610.json index 9f714d53333..54384dcff3c 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-auto-open-preview-panel-when-workflow-is-built/1775805992921-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-auto-open-preview-panel-when-workflow-is-built/1775805992921-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-close-preview-panel-via-close-button/1775805998005-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-close-preview-panel-via-close-button/1775805998005-unknown-host-POST-_v1_messages-18622610.json index 3cdd68e14e2..ec5dc47dde9 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-close-preview-panel-via-close-button/1775805998005-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-close-preview-panel-via-close-button/1775805998005-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-create-new-thread-via-sidebar-button/1775805937822-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-create-new-thread-via-sidebar-button/1775805937822-unknown-host-POST-_v1_messages-18622610.json index 3cb1b98bd4d..71090dfa555 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-create-new-thread-via-sidebar-button/1775805937822-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-create-new-thread-via-sidebar-button/1775805937822-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-delete-thread-via-action-menu/1775805968314-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-delete-thread-via-action-menu/1775805968314-unknown-host-POST-_v1_messages-18622610.json index 4681820dc0f..708ed4d609e 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-delete-thread-via-action-menu/1775805968314-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-delete-thread-via-action-menu/1775805968314-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-display-artifact-card-in-timeline-after-workflow-build/1775805936944-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-display-artifact-card-in-timeline-after-workflow-build/1775805936944-unknown-host-POST-_v1_messages-18622610.json index b6c22b5e727..da6a29097e3 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-display-artifact-card-in-timeline-after-workflow-build/1775805936944-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-display-artifact-card-in-timeline-after-workflow-build/1775805936944-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-display-canvas-nodes-in-preview-iframe/1775805998608-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-display-canvas-nodes-in-preview-iframe/1775805998608-unknown-host-POST-_v1_messages-18622610.json index 5059201e5dc..70a94f9f323 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-display-canvas-nodes-in-preview-iframe/1775805998608-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-display-canvas-nodes-in-preview-iframe/1775805998608-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-open-workflow-preview-when-clicking-artifact-card/1775805949876-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-open-workflow-preview-when-clicking-artifact-card/1775805949876-unknown-host-POST-_v1_messages-18622610.json index 14e0fe61289..88064ea2f4e 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-open-workflow-preview-when-clicking-artifact-card/1775805949876-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-open-workflow-preview-when-clicking-artifact-card/1775805949876-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-persist-messages-after-page-reload/1775805923368-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-persist-messages-after-page-reload/1775805923368-unknown-host-POST-_v1_messages-18622610.json index 06ed1519e5a..770e00c11dd 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-persist-messages-after-page-reload/1775805923368-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-persist-messages-after-page-reload/1775805923368-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-approve-workflow-execution/1775805949061-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-approve-workflow-execution/1775805949061-unknown-host-POST-_v1_messages-18622610.json index 97c799573a6..ca6acf318e7 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-approve-workflow-execution/1775805949061-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-approve-workflow-execution/1775805949061-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-deny-workflow-execution/1775805967569-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-deny-workflow-execution/1775805967569-unknown-host-POST-_v1_messages-18622610.json index 8ee0eeee150..f1fac7e0432 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-deny-workflow-execution/1775805967569-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-show-approval-panel-and-deny-workflow-execution/1775805967569-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995521-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995521-unknown-host-POST-_v1_messages-18622610.json index 994d6d69b9f..c2bf882e92b 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995521-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995521-unknown-host-POST-_v1_messages-18622610.json @@ -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 } }, diff --git a/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995522-unknown-host-POST-_v1_messages-18622610.json b/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995522-unknown-host-POST-_v1_messages-18622610.json index 2bac77874a6..0bf9c3aedfd 100644 --- a/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995522-unknown-host-POST-_v1_messages-18622610.json +++ b/packages/testing/playwright/expectations/instance-ai/should-switch-between-threads/1775805995522-unknown-host-POST-_v1_messages-18622610.json @@ -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 } },