mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(core): Address chat feedback items (no-changelog) (#22725)
This commit is contained in:
@@ -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,
|
||||
|
||||
+3
-1
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user