fix(core): Address chat feedback items (no-changelog) (#22725)

This commit is contained in:
Jaakko Husso
2025-12-04 15:17:59 +02:00
committed by GitHub
parent 0866f644b1
commit f3fa3f9c30
9 changed files with 101 additions and 15 deletions
+25 -1
View File
@@ -267,6 +267,27 @@ export const chatAttachmentSchema = z.object({
fileName: z.string(),
});
export const isValidTimeZone = (tz: string): boolean => {
try {
// Throws if invalid timezone
new Intl.DateTimeFormat('en-US', { timeZone: tz });
return true;
} catch {
return false;
}
};
export const StrictTimeZoneSchema = z
.string()
.min(1)
.max(50)
.regex(/^[A-Za-z0-9_/+-]+$/)
.refine(isValidTimeZone, {
message: 'Unknown or invalid time zone',
});
export const TimeZoneSchema = StrictTimeZoneSchema.optional().catch(undefined);
export type ChatAttachment = z.infer<typeof chatAttachmentSchema>;
export class ChatHubSendMessageRequest extends Z.class({
@@ -283,7 +304,8 @@ export class ChatHubSendMessageRequest extends Z.class({
),
tools: z.array(INodeSchema),
attachments: z.array(chatAttachmentSchema),
agentName: z.string(),
agentName: z.string().optional(),
timeZone: TimeZoneSchema,
}) {}
export class ChatHubRegenerateMessageRequest extends Z.class({
@@ -294,6 +316,7 @@ export class ChatHubRegenerateMessageRequest extends Z.class({
name: z.string(),
}),
),
timeZone: TimeZoneSchema,
}) {}
export class ChatHubEditMessageRequest extends Z.class({
@@ -306,6 +329,7 @@ export class ChatHubEditMessageRequest extends Z.class({
name: z.string(),
}),
),
timeZone: TimeZoneSchema,
}) {}
export class ChatHubUpdateConversationRequest extends Z.class({
@@ -323,7 +323,7 @@ export class ChatTrigger extends Node {
},
{
displayName:
'Chat will be live at the URL above once you activate this workflow. Live executions will show up in the ‘executions’ tab',
'Chat will be live at the URL above once this workflow is published. Live executions will show up in the ‘executions’ tab',
name: 'hostedChatNotice',
type: 'notice',
displayOptions: {
@@ -336,7 +336,7 @@ export class ChatTrigger extends Node {
},
{
displayName:
'Follow the instructions <a href="https://www.npmjs.com/package/@n8n/chat" target="_blank">here</a> to embed chat in a webpage (or just call the webhook URL at the top of this section). Chat will be live once you activate this workflow',
'Follow the instructions <a href="https://www.npmjs.com/package/@n8n/chat" target="_blank">here</a> to embed chat in a webpage (or just call the webhook URL at the top of this section). Chat will be live once you publish this workflow',
name: 'embeddedChatNotice',
type: 'notice',
displayOptions: {
@@ -406,6 +406,18 @@ export class ChatTrigger extends Node {
},
},
},
{
displayName:
'Your n8n users will be able to use this agent in <a href="/home/chat/" target="_blank">Chat</a> once this workflow is published. Make sure to share this workflow with at least viewer access to all users who should use it. Currently, only streaming response mode is supported.',
name: 'availableInChatNotice',
type: 'notice',
displayOptions: {
show: {
availableInChat: [true],
},
},
default: '',
},
{
displayName: 'Agent Name',
name: 'agentName',
@@ -9,6 +9,7 @@ import {
} from '@n8n/db';
import { Service } from '@n8n/di';
import { EntityManager } from '@n8n/typeorm';
import { DateTime } from 'luxon';
import {
AGENT_LANGCHAIN_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
@@ -52,6 +53,7 @@ export class ChatHubWorkflowService {
model: ChatHubConversationModel,
systemMessage: string | undefined,
tools: INode[],
timeZone: string,
trx?: EntityManager,
): Promise<{ workflowData: IWorkflowBase; executionData: IRunExecutionData }> {
return await withTransaction(this.workflowRepository.manager, trx, async (em) => {
@@ -69,6 +71,7 @@ export class ChatHubWorkflowService {
model,
systemMessage,
tools,
timeZone,
});
const newWorkflow = new WorkflowEntity();
@@ -258,6 +261,7 @@ export class ChatHubWorkflowService {
model,
systemMessage,
tools,
timeZone,
}: {
userId: string;
sessionId: ChatSessionId;
@@ -268,9 +272,10 @@ export class ChatHubWorkflowService {
model: ChatHubConversationModel;
systemMessage?: string;
tools: INode[];
timeZone: string;
}) {
const chatTriggerNode = this.buildChatTriggerNode();
const toolsAgentNode = this.buildToolsAgentNode(model, systemMessage);
const toolsAgentNode = this.buildToolsAgentNode(model, timeZone, systemMessage);
const modelNode = this.buildModelNode(credentials, model);
const memoryNode = this.buildMemoryNode(20);
const restoreMemoryNode = this.buildRestoreMemoryNode(history);
@@ -463,7 +468,24 @@ export class ChatHubWorkflowService {
};
}
private buildToolsAgentNode(model: ChatHubConversationModel, systemMessage?: string): INode {
getSystemMessageMetadata(timeZone: string) {
const now = DateTime.now().setZone(timeZone).toISO({
includeOffset: true,
});
return `The user's current local date and time is: ${now} (timezone: ${timeZone}).
When you need to reference “now”, use this date and time.`;
}
private getBaseSystemMessage(timeZone: string) {
return 'You are a helpful assistant.\n' + this.getSystemMessageMetadata(timeZone);
}
private buildToolsAgentNode(
model: ChatHubConversationModel,
timeZone: string,
systemMessage?: string,
): INode {
return {
parameters: {
promptType: 'define',
@@ -474,7 +496,7 @@ export class ChatHubWorkflowService {
model.provider !== 'n8n' && model.provider !== 'custom-agent'
? getMaxContextWindowTokens(model.provider, model.model)
: undefined,
systemMessage,
systemMessage: systemMessage ?? this.getBaseSystemMessage(timeZone),
},
},
type: AGENT_LANGCHAIN_NODE_TYPE,
@@ -23,6 +23,7 @@ import { Logger } from '@n8n/backend-common';
import { ExecutionRepository, IExecutionResponse, User, WorkflowRepository, In } from '@n8n/db';
import { Service } from '@n8n/di';
import type { EntityManager } from '@n8n/typeorm';
import { GlobalConfig } from '@n8n/config';
import type { Response } from 'express';
import { ErrorReporter, InstanceSettings } from 'n8n-core';
import {
@@ -104,6 +105,7 @@ export class ChatHubService {
private readonly chatHubSettingsService: ChatHubSettingsService,
private readonly chatHubAttachmentService: ChatHubAttachmentService,
private readonly instanceSettings: InstanceSettings,
private readonly globalConfig: GlobalConfig,
) {}
async getModels(
@@ -1029,7 +1031,9 @@ export class ChatHubService {
previousMessageId,
tools,
attachments,
timeZone,
} = payload;
const tz = timeZone ?? this.globalConfig.generic.timezone;
const credentialId = this.getModelCredential(model, credentials);
@@ -1079,6 +1083,7 @@ export class ChatHubService {
message,
tools,
processedAttachments,
tz,
trx,
);
});
@@ -1125,7 +1130,8 @@ export class ChatHubService {
}
async editMessage(res: Response, user: User, payload: EditMessagePayload) {
const { sessionId, editId, messageId, message, model, credentials } = payload;
const { sessionId, editId, messageId, message, model, credentials, timeZone } = payload;
const tz = timeZone ?? this.globalConfig.generic.timezone;
const workflow = await this.messageRepository.manager.transaction(async (trx) => {
const session = await this.getChatSession(user, sessionId, trx);
@@ -1170,6 +1176,7 @@ export class ChatHubService {
message,
session.tools,
attachments,
tz,
trx,
);
}
@@ -1195,7 +1202,8 @@ export class ChatHubService {
}
async regenerateAIMessage(res: Response, user: User, payload: RegenerateMessagePayload) {
const { sessionId, retryId, model, credentials } = payload;
const { sessionId, retryId, model, credentials, timeZone } = payload;
const tz = timeZone ?? this.globalConfig.generic.timezone;
const {
workflow: { workflowData, executionData },
@@ -1242,6 +1250,7 @@ export class ChatHubService {
message,
session.tools,
attachments,
tz,
trx,
);
@@ -1273,6 +1282,7 @@ export class ChatHubService {
message: string,
tools: INode[],
attachments: IBinaryData[],
timeZone: string,
trx: EntityManager,
) {
if (model.provider === 'n8n') {
@@ -1293,6 +1303,7 @@ export class ChatHubService {
history,
message,
attachments,
timeZone,
trx,
);
}
@@ -1307,6 +1318,7 @@ export class ChatHubService {
undefined,
tools,
attachments,
timeZone,
trx,
);
}
@@ -1321,6 +1333,7 @@ export class ChatHubService {
systemMessage: string | undefined,
tools: INode[],
attachments: IBinaryData[],
timeZone: string,
trx: EntityManager,
) {
await this.chatHubSettingsService.ensureModelIsAllowed(model);
@@ -1342,6 +1355,7 @@ export class ChatHubService {
model,
systemMessage,
tools,
timeZone,
trx,
);
}
@@ -1353,6 +1367,7 @@ export class ChatHubService {
history: ChatHubMessage[],
message: string,
attachments: IBinaryData[],
timeZone: string,
trx: EntityManager,
) {
const agent = await this.chatHubAgentService.getAgentById(agentId, user.id);
@@ -1370,7 +1385,8 @@ export class ChatHubService {
throw new BadRequestError('Credentials not set for agent');
}
const systemMessage = agent.systemPrompt;
const systemMessage =
agent.systemPrompt + '\n' + this.chatHubWorkflowService.getSystemMessageMetadata(timeZone);
const model: ChatHubBaseLLMModel = {
provider: agent.provider,
@@ -1396,6 +1412,7 @@ export class ChatHubService {
systemMessage,
tools,
attachments,
timeZone,
trx,
);
}
@@ -2074,7 +2091,7 @@ export class ChatHubService {
model: ChatHubConversationModel,
credentialId: string | null,
tools: INode[],
agentName: string,
agentName?: string,
trx?: EntityManager,
) {
await this.ensureValidModel(user, model);
@@ -22,6 +22,7 @@ export interface BaseMessagePayload {
sessionId: ChatSessionId;
model: ChatHubConversationModel;
credentials: INodeCredentials;
timeZone?: string;
}
export interface HumanMessagePayload extends BaseMessagePayload {
@@ -30,7 +31,7 @@ export interface HumanMessagePayload extends BaseMessagePayload {
previousMessageId: ChatMessageId | null;
attachments: ChatAttachment[];
tools: INode[];
agentName: string;
agentName?: string;
}
export interface RegenerateMessagePayload extends BaseMessagePayload {
retryId: ChatMessageId;
@@ -566,6 +566,7 @@ function onFilesDropped(files: File[]) {
:selected-model="selectedModel"
:credentials="credentialsByProvider"
:ready-to-show-model-selector="isNewSession || !!currentConversation"
:is-new-session="isNewSession"
@select-model="handleSelectModel"
@edit-custom-agent="handleEditAgent"
@create-custom-agent="openNewAgentCreator"
@@ -548,6 +548,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
tools,
attachments,
agentName,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
onStreamMessage,
onStreamDone,
@@ -618,6 +619,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
messageId: promptId,
message: content,
credentials,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
onStreamMessage,
onStreamDone,
@@ -654,6 +656,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
{
model,
credentials,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
onStreamMessage,
onStreamDone,
@@ -17,7 +17,8 @@ import { useI18n } from '@n8n/i18n';
import { computed, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
const { selectedModel, credentials, readyToShowModelSelector } = defineProps<{
const { isNewSession, selectedModel, credentials, readyToShowModelSelector } = defineProps<{
isNewSession: boolean;
selectedModel: ChatModelDto | null;
credentials: CredentialsMap | null;
readyToShowModelSelector: boolean;
@@ -79,6 +80,7 @@ defineExpose({
text
icon-size="large"
:aria-label="i18n.baseText('chatHub.chat.header.button.newChat')"
:disabled="isNewSession"
@click="onNewChat"
/>
<ModelSelector
@@ -156,13 +156,13 @@ const menu = computed(() => {
id: provider,
title: providerDisplayNames[provider],
submenu: [
configureMenu,
{ isDivider: true as const, id: 'divider' },
{
id: `${provider}::loading`,
title: i18n.baseText('generic.loadingEllipsis'),
disabled: true,
},
{ isDivider: true as const, id: 'divider' },
configureMenu,
],
});
continue;
@@ -234,9 +234,13 @@ const menu = computed(() => {
} as const,
]
: []),
configureMenu,
]);
submenu.unshift(
configureMenu,
...(submenu.length > 1 ? [{ isDivider: true as const, id: 'divider' }] : []),
);
menuItems.push({
id: provider,
title: providerDisplayNames[provider],